diff options
author | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:46:06 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:48:30 -0300 |
commit | 38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch) | |
tree | 453dbf70000cc5e338b06201af1eaca8343f8f73 /@linaria/packages/babel/__tests__ | |
parent | f26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff) | |
download | node-vendor-master.tar.gz node-vendor-master.tar.bz2 node-vendor-master.zip |
Diffstat (limited to '@linaria/packages/babel/__tests__')
14 files changed, 3197 insertions, 0 deletions
diff --git a/@linaria/packages/babel/__tests__/__snapshots__/babel.test.ts.snap b/@linaria/packages/babel/__tests__/__snapshots__/babel.test.ts.snap new file mode 100644 index 0000000..84be71a --- /dev/null +++ b/@linaria/packages/babel/__tests__/__snapshots__/babel.test.ts.snap @@ -0,0 +1,617 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`does not include styles if not referenced anywhere 1`] = ` +"import { css } from '@linaria/core'; +import { styled } from '@linaria/react'; +const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"t17gu1mi\\" +}); +const title = \\"tccybe9\\";" +`; + +exports[`does not include styles if not referenced anywhere 2`] = `Object {}`; + +exports[`does not output CSS if none present 1`] = ` +"const number = 42; +const title = String.raw\`This is something\`;" +`; + +exports[`does not output CSS if none present 2`] = `Object {}`; + +exports[`does not output CSS property when value is a blank string 1`] = ` +"import { css } from '@linaria/core'; +export const title = \\"t17gu1mi\\";" +`; + +exports[`does not output CSS property when value is a blank string 2`] = ` + +CSS: + +.t17gu1mi { + font-size: ; + margin: 6px; +} + +Dependencies: NA + +`; + +exports[`evaluates and inlines expressions in scope 1`] = ` +"import { styled } from '@linaria/react'; +const color = 'blue'; +export const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"t17gu1mi\\" +});" +`; + +exports[`evaluates and inlines expressions in scope 2`] = ` + +CSS: + +.t17gu1mi { + color: blue; + width: 33.333333333333336%; +} + +Dependencies: NA + +`; + +exports[`handles css template literal in JSX element 1`] = ` +"import { css } from '@linaria/core'; +<Title class={\\"t17gu1mi\\"} />;" +`; + +exports[`handles css template literal in JSX element 2`] = ` + +CSS: + +.t17gu1mi { font-size: 14px; } + +Dependencies: NA + +`; + +exports[`handles css template literal in object property 1`] = ` +"import { css } from '@linaria/core'; +const components = { + title: \\"t17gu1mi\\" +};" +`; + +exports[`handles css template literal in object property 2`] = ` + +CSS: + +.t17gu1mi { + font-size: 14px; + } + +Dependencies: NA + +`; + +exports[`handles fn passed in as classNameSlug 1`] = ` +"import { styled } from '@linaria/react'; +export const Title = /*#__PURE__*/styled('h1')({ + name: \\"Title\\", + class: \\"t17gu1mi_42_Title\\" +});" +`; + +exports[`handles fn passed in as classNameSlug 2`] = ` + +CSS: + +.t17gu1mi_42_Title { + font-size: 14px; +} + +Dependencies: NA + +`; + +exports[`handles interpolation followed by unit 1`] = ` +"import { styled } from '@linaria/react'; +export const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"t17gu1mi\\", + vars: { + \\"t17gu1mi-0\\": [size, \\"em\\"], + \\"t17gu1mi-1\\": [shadow, \\"px\\"], + \\"t17gu1mi-2\\": [size, \\"px\\"], + \\"t17gu1mi-3\\": [props => props.width, \\"vw\\"], + \\"t17gu1mi-4\\": [props => { + if (true) { + return props.height; + } else { + return 200; + } + }, \\"px\\"], + \\"t17gu1mi-5\\": [unit, \\"fr\\"], + \\"t17gu1mi-7\\": [function (props) { + return 200; + }, \\"px\\"] + } +});" +`; + +exports[`handles interpolation followed by unit 2`] = ` + +CSS: + +.t17gu1mi { + font-size: var(--t17gu1mi-0); + text-shadow: black 1px var(--t17gu1mi-1), white -2px -2px; + margin: var(--t17gu1mi-2); + width: calc(2 * var(--t17gu1mi-3)); + height: var(--t17gu1mi-4); + grid-template-columns: var(--t17gu1mi-5) 1fr 1fr var(--t17gu1mi-5); + border-radius: var(--t17gu1mi-7) +} + +Dependencies: NA + +`; + +exports[`handles nested blocks 1`] = ` +"import { styled } from '@linaria/react'; +export const Button = /*#__PURE__*/styled(\\"button\\")({ + name: \\"Button\\", + class: \\"b17gu1mi\\", + vars: { + \\"b17gu1mi-0\\": [regular] + } +});" +`; + +exports[`handles nested blocks 2`] = ` + +CSS: + +.b17gu1mi { + font-family: var(--b17gu1mi-0); + + &:hover { + border-color: blue; + } + + @media (max-width: 200px) { + width: 100%; + } +} + +Dependencies: NA + +`; + +exports[`handles objects with enums as keys 1`] = ` +"import { css } from '@linaria/core'; +import { TestEnum } from './ts-data.ts'; +export const object = { + [TestEnum.FirstValue]: \\"t17gu1mi\\", + [TestEnum.SecondValue]: \\"tccybe9\\" +};" +`; + +exports[`handles objects with enums as keys 2`] = ` + +CSS: + +.t17gu1mi {} +.tccybe9 {} + +Dependencies: NA + +`; + +exports[`handles objects with numeric keys 1`] = ` +"import { css } from '@linaria/core'; +export const object = { + stringKey: \\"s17gu1mi\\", + 42: \\"_ccybe9\\" +};" +`; + +exports[`handles objects with numeric keys 2`] = ` + +CSS: + +.s17gu1mi {} +._ccybe9 {} + +Dependencies: NA + +`; + +exports[`includes unreferenced styles for :global 1`] = ` +"import { css } from '@linaria/core'; +import { styled } from '@linaria/react'; +const a = \\"a17gu1mi\\"; +const B = /*#__PURE__*/styled(\\"div\\")({ + name: \\"B\\", + class: \\"bccybe9\\" +});" +`; + +exports[`includes unreferenced styles for :global 2`] = ` + +CSS: + +.a17gu1mi { + :global() { + .title { + font-size: 14px; + } + } +} +.bccybe9 { + :global(.title) { + font-size: 14px; + } +} + +Dependencies: NA + +`; + +exports[`inlines array styles as CSS string 1`] = ` +"import { styled } from '@linaria/react'; +const styles = [{ + flex: 1 +}, { + display: 'block', + height: 24 +}]; +export const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"t17gu1mi\\" +});" +`; + +exports[`inlines array styles as CSS string 2`] = ` + +CSS: + +.t17gu1mi { + flex: 1; display: block; height: 24px; +} + +Dependencies: NA + +`; + +exports[`inlines object styles as CSS string 1`] = ` +"import { styled } from '@linaria/react'; +const cover = { + '--color-primaryText': '#222', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + opacity: 1, + minHeight: 420, + '&.shouldNotBeChanged': { + borderColor: '#fff' + }, + '@media (min-width: 200px)': { + WebkitOpacity: .8, + MozOpacity: .8, + msOpacity: .8, + OOpacity: .8, + WebkitBorderRadius: 2, + MozBorderRadius: 2, + msBorderRadius: 2, + OBorderRadius: 2, + WebkitTransition: '400ms', + MozTransition: '400ms', + OTransition: '400ms', + msTransition: '400ms' + } +}; +export const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"t17gu1mi\\" +});" +`; + +exports[`inlines object styles as CSS string 2`] = ` + +CSS: + +.t17gu1mi { + --color-primaryText: #222; position: absolute; top: 0; right: 0; bottom: 0; left: 0; opacity: 1; min-height: 420px; &.shouldNotBeChanged { border-color: #fff; } @media (min-width: 200px) { -webkit-opacity: 0.8; -moz-opacity: 0.8; -ms-opacity: 0.8; -o-opacity: 0.8; -webkit-border-radius: 2px; -moz-border-radius: 2px; -ms-border-radius: 2px; -o-border-radius: 2px; -webkit-transition: 400ms; -moz-transition: 400ms; -o-transition: 400ms; -ms-transition: 400ms; } +} + +Dependencies: NA + +`; + +exports[`outputs valid CSS classname 1`] = ` +"import { styled } from '@linaria/react'; +export const ΩPage$Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"\\\\u03A9Page$Title\\", + class: \\"\\\\u03C917gu1mi\\" +});" +`; + +exports[`outputs valid CSS classname 2`] = ` + +CSS: + +.ω17gu1mi { + font-size: 14px; +} + +Dependencies: NA + +`; + +exports[`prevents class name collision 1`] = ` +"import { styled } from '@linaria/react'; +export const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"t17gu1mi\\", + vars: { + \\"t17gu1mi-0\\": [size, \\"px\\"], + \\"t17gu1mi-1\\": [props => props.color] + } +}); + +function Something() { + const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"tccybe9\\", + vars: { + \\"tccybe9-0\\": [regular] + } + }); + return <Title />; +}" +`; + +exports[`prevents class name collision 2`] = ` + +CSS: + +.t17gu1mi { + font-size: var(--t17gu1mi-0); + color: var(--t17gu1mi-1) +} +.tccybe9 { + font-family: var(--tccybe9-0); + } + +Dependencies: NA + +`; + +exports[`replaces unknown expressions with CSS custom properties 1`] = ` +"import { styled } from '@linaria/react'; +export const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"t17gu1mi\\", + vars: { + \\"t17gu1mi-0\\": [size, \\"px\\"], + \\"t17gu1mi-1\\": [props => props.color] + } +});" +`; + +exports[`replaces unknown expressions with CSS custom properties 2`] = ` + +CSS: + +.t17gu1mi { + font-size: var(--t17gu1mi-0); + color: var(--t17gu1mi-1); +} + +Dependencies: NA + +`; + +exports[`supports both css and styled tags 1`] = ` +"import { css } from '@linaria/core'; +import { styled } from '@linaria/react'; +export const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"t17gu1mi\\" +}); +export const title = \\"tccybe9\\";" +`; + +exports[`supports both css and styled tags 2`] = ` + +CSS: + +.t17gu1mi { + font-size: 14px; +} +.tccybe9 { + color: blue; +} + +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. + 2 | + 3 | const title = css\` +> 4 | font-size: \${size}px; + | ^^^^ + 5 | \`;" +`; + +exports[`transpiles css template literal 1`] = ` +"import { css } from '@linaria/core'; +export const title = \\"t17gu1mi\\";" +`; + +exports[`transpiles css template literal 2`] = ` + +CSS: + +.t17gu1mi { + font-size: 14px; +} + +Dependencies: NA + +`; + +exports[`transpiles renamed styled import 1`] = ` +"import { styled as custom } from '@linaria/react'; +export const Title = /*#__PURE__*/custom('h1')({ + name: \\"Title\\", + class: \\"t17gu1mi\\" +});" +`; + +exports[`transpiles renamed styled import 2`] = ` + +CSS: + +.t17gu1mi { + font-size: 14px; +} + +Dependencies: NA + +`; + +exports[`transpiles styled template literal with function and component 1`] = ` +"import { styled } from '@linaria/react'; + +const Heading = () => null; + +export const Title = /*#__PURE__*/styled(Heading)({ + name: \\"Title\\", + class: \\"t17gu1mi\\" +});" +`; + +exports[`transpiles styled template literal with function and component 2`] = ` + +CSS: + +.t17gu1mi { + font-size: 14px; +} + +Dependencies: NA + +`; + +exports[`transpiles styled template literal with function and tag 1`] = ` +"import { styled } from '@linaria/react'; +export const Title = /*#__PURE__*/styled('h1')({ + name: \\"Title\\", + class: \\"t17gu1mi\\" +});" +`; + +exports[`transpiles styled template literal with function and tag 2`] = ` + +CSS: + +.t17gu1mi { + font-size: 14px; +} + +Dependencies: NA + +`; + +exports[`transpiles styled template literal with object 1`] = ` +"import { styled } from '@linaria/react'; +export const Title = /*#__PURE__*/styled(\\"h1\\")({ + name: \\"Title\\", + class: \\"t17gu1mi\\" +});" +`; + +exports[`transpiles styled template literal with object 2`] = ` + +CSS: + +.t17gu1mi { + font-size: 14px; +} + +Dependencies: NA + +`; + +exports[`uses string passed in as classNameSlug 1`] = ` +"import { styled } from '@linaria/react'; +export const Title = /*#__PURE__*/styled('h1')({ + name: \\"Title\\", + class: \\"testSlug\\" +});" +`; + +exports[`uses string passed in as classNameSlug 2`] = ` + +CSS: + +.testSlug { + font-size: 14px; +} + +Dependencies: NA + +`; + +exports[`uses the same custom property for the same expression 1`] = ` +"import { styled } from '@linaria/react'; +export const Box = /*#__PURE__*/styled(\\"div\\")({ + name: \\"Box\\", + class: \\"b17gu1mi\\", + vars: { + \\"b17gu1mi-0\\": [props => props.size, \\"px\\"] + } +});" +`; + +exports[`uses the same custom property for the same expression 2`] = ` + +CSS: + +.b17gu1mi { + height: var(--b17gu1mi-0); + width: var(--b17gu1mi-0); +} + +Dependencies: NA + +`; + +exports[`uses the same custom property for the same identifier 1`] = ` +"import { styled } from '@linaria/react'; +export const Box = /*#__PURE__*/styled(\\"div\\")({ + name: \\"Box\\", + class: \\"b17gu1mi\\", + vars: { + \\"b17gu1mi-0\\": [size, \\"px\\"] + } +});" +`; + +exports[`uses the same custom property for the same identifier 2`] = ` + +CSS: + +.b17gu1mi { + height: var(--b17gu1mi-0); + width: var(--b17gu1mi-0); +} + +Dependencies: NA + +`; diff --git a/@linaria/packages/babel/__tests__/__snapshots__/dynamic-import-noop.test.js.snap b/@linaria/packages/babel/__tests__/__snapshots__/dynamic-import-noop.test.js.snap new file mode 100644 index 0000000..02d59d1 --- /dev/null +++ b/@linaria/packages/babel/__tests__/__snapshots__/dynamic-import-noop.test.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`replaces dynamic imports with a noop 1`] = ` +"({ + then: () => undefined, + catch: () => undefined +}).then(foo => foo.init());" +`; diff --git a/@linaria/packages/babel/__tests__/__snapshots__/transform.test.ts.snap b/@linaria/packages/babel/__tests__/__snapshots__/transform.test.ts.snap new file mode 100644 index 0000000..77636ca --- /dev/null +++ b/@linaria/packages/babel/__tests__/__snapshots__/transform.test.ts.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`doesn't rewrite an absolute path in url() declarations 1`] = ` +".tpyglzj{background-image:url(/assets/test.jpg);} +" +`; + +exports[`rewrites a relative path in url() declarations 1`] = ` +".tpyglzj{background-image:url(../assets/test.jpg);background-image:url(\\"../assets/test.jpg\\");background-image:url('../assets/test.jpg');} +" +`; + +exports[`rewrites multiple relative paths in url() declarations 1`] = ` +"@font-face{font-family:Test;src:url(../assets/font.woff2) format(\\"woff2\\"),url(../assets/font.woff) format(\\"woff\\");} +" +`; diff --git a/@linaria/packages/babel/__tests__/babel.test.ts b/@linaria/packages/babel/__tests__/babel.test.ts new file mode 100644 index 0000000..99cc816 --- /dev/null +++ b/@linaria/packages/babel/__tests__/babel.test.ts @@ -0,0 +1,524 @@ +import { join } from 'path'; +import { transformAsync } from '@babel/core'; +import dedent from 'dedent'; +import stripAnsi from 'strip-ansi'; +import type { StrictOptions } from '../src'; +import serializer from '../__utils__/linaria-snapshot-serializer'; + +expect.addSnapshotSerializer(serializer); + +const transpile = async ( + input: string, + opts: Partial<StrictOptions> = { evaluate: false } +) => + (await transformAsync(input, { + babelrc: false, + presets: [[require.resolve('../src'), opts]], + plugins: ['@babel/plugin-syntax-jsx'], + filename: join(__dirname, 'app/index.js'), + configFile: false, + }))!; + +it('transpiles styled template literal with object', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Title = styled.h1\` + font-size: 14px; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('uses string passed in as classNameSlug', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Title = styled('h1')\` + font-size: 14px; + \`; +`, + { classNameSlug: 'testSlug' } + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('handles fn passed in as classNameSlug', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Title = styled('h1')\` + font-size: 14px; + \`; +`, + { + classNameSlug: (hash, title) => { + return `${hash}_${7 * 6}_${title}`; + }, + } + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('transpiles styled template literal with function and tag', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Title = styled('h1')\` + font-size: 14px; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('transpiles renamed styled import', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled as custom } from '@linaria/react'; + + export const Title = custom('h1')\` + font-size: 14px; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('transpiles styled template literal with function and component', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + const Heading = () => null; + + export const Title = styled(Heading)\` + font-size: 14px; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('outputs valid CSS classname', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const ΩPage$Title = styled.h1\` + font-size: 14px; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('evaluates and inlines expressions in scope', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + const color = 'blue'; + + export const Title = styled.h1\` + color: ${'${color}'}; + width: ${'${100 / 3}'}%; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('inlines object styles as CSS string', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + const cover = { + '--color-primaryText': '#222', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + opacity: 1, + minHeight: 420, + + '&.shouldNotBeChanged': { + borderColor: '#fff', + }, + + '@media (min-width: 200px)': { + WebkitOpacity: .8, + MozOpacity: .8, + msOpacity: .8, + OOpacity: .8, + WebkitBorderRadius: 2, + MozBorderRadius: 2, + msBorderRadius: 2, + OBorderRadius: 2, + WebkitTransition: '400ms', + MozTransition: '400ms', + OTransition: '400ms', + msTransition: '400ms', + } + }; + + export const Title = styled.h1\` + ${'${cover}'} + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('inlines array styles as CSS string', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + const styles = [ + { flex: 1 }, + { display: 'block', height: 24 }, + ]; + + export const Title = styled.h1\` + ${'${styles}'} + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('replaces unknown expressions with CSS custom properties', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Title = styled.h1\` + font-size: ${'${size}'}px; + color: ${'${props => props.color}'}; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('handles interpolation followed by unit', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Title = styled.h1\` + font-size: ${'${size}'}em; + text-shadow: black 1px ${'${shadow}'}px, white -2px -2px; + margin: ${'${size}'}px; + width: calc(2 * ${'${props => props.width}'}vw); + height: ${'${props => { if (true) { return props.height } else { return 200 } }}'}px; + grid-template-columns: ${'${unit}'}fr 1fr 1fr ${'${unit}'}fr; + border-radius: ${'${function(props) { return 200 }}'}px + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('uses the same custom property for the same identifier', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Box = styled.div\` + height: ${'${size}'}px; + width: ${'${size}'}px; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('uses the same custom property for the same expression', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Box = styled.div\` + height: ${'${props => props.size}'}px; + width: ${'${props => props.size}'}px; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('handles nested blocks', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Button = styled.button\` + font-family: ${'${regular}'}; + + &:hover { + border-color: blue; + } + + @media (max-width: 200px) { + width: 100%; + } + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('prevents class name collision', async () => { + const { code, metadata } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + export const Title = styled.h1\` + font-size: ${'${size}'}px; + color: ${'${props => props.color}'} + \`; + + function Something() { + const Title = styled.h1\` + font-family: ${'${regular}'}; + \`; + + return <Title />; + } + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('does not output CSS if none present', async () => { + const { code, metadata } = await transpile( + dedent` + const number = 42; + + const title = String.raw\`This is something\`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('does not output CSS property when value is a blank string', async () => { + const { code, metadata } = await transpile( + dedent` + import { css } from '@linaria/core'; + + export const title = css\` + font-size: ${''}; + margin: 6px; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('transpiles css template literal', async () => { + const { code, metadata } = await transpile( + dedent` + import { css } from '@linaria/core'; + + export const title = css\` + font-size: 14px; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('handles css template literal in object property', async () => { + const { code, metadata } = await transpile( + dedent` + import { css } from '@linaria/core'; + + const components = { + title: css\` + font-size: 14px; + \` + }; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('handles css template literal in JSX element', async () => { + const { code, metadata } = await transpile( + dedent` + import { css } from '@linaria/core'; + + <Title class={css\` font-size: 14px; \`} /> + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('throws when contains dynamic expression without evaluate: true in css tag', async () => { + expect.assertions(1); + + try { + await transpile( + dedent` + import { css } from '@linaria/core'; + + const title = css\` + font-size: ${'${size}'}px; + \`; + ` + ); + } catch (e) { + expect( + stripAnsi(e.message.replace(__dirname, '<<DIRNAME>>')) + ).toMatchSnapshot(); + } +}); + +it('supports both css and styled tags', async () => { + const { code, metadata } = await transpile( + dedent` + import { css } from '@linaria/core'; + import { styled } from '@linaria/react'; + + export const Title = styled.h1\` + font-size: 14px; + \`; + + export const title = css\` + color: blue; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('does not include styles if not referenced anywhere', async () => { + const { code, metadata } = await transpile( + dedent` + import { css } from '@linaria/core'; + import { styled } from '@linaria/react'; + + const Title = styled.h1\` + font-size: 14px; + \`; + + const title = css\` + color: blue; + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('includes unreferenced styles for :global', async () => { + const { code, metadata } = await transpile( + dedent` + import { css } from '@linaria/core'; + import { styled } from '@linaria/react'; + + const a = css\` + :global() { + .title { + font-size: 14px; + } + } + \`; + + const B = styled.div\` + :global(.title) { + font-size: 14px; + } + \`; + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('handles objects with numeric keys', async () => { + const { code, metadata } = await transpile( + dedent` + import { css } from '@linaria/core'; + + export const object = { + stringKey: css\`\`, + 42: css\`\`, + } + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); + +it('handles objects with enums as keys', async () => { + const { code, metadata } = await transpile( + dedent` + import { css } from '@linaria/core'; + import { TestEnum } from './ts-data.ts'; + + export const object = { + [TestEnum.FirstValue]: css\`\`, + [TestEnum.SecondValue]: css\`\`, + } + ` + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); +}); diff --git a/@linaria/packages/babel/__tests__/depsGraph.test.ts b/@linaria/packages/babel/__tests__/depsGraph.test.ts new file mode 100644 index 0000000..000bd30 --- /dev/null +++ b/@linaria/packages/babel/__tests__/depsGraph.test.ts @@ -0,0 +1,356 @@ +/* eslint-disable no-template-curly-in-string */ + +import dedent from 'dedent'; +import * as babel from '@babel/core'; +import buildDepsGraph from '../../shaker/src/graphBuilder'; + +function _build(literal: TemplateStringsArray, ...placeholders: string[]) { + const code = dedent(literal, ...placeholders); + return { + ast: babel.parseSync(code, { filename: 'source.js' })!, + code, + }; +} + +function _buildGraph(literal: TemplateStringsArray, ...placeholders: string[]) { + const { ast } = _build(literal, ...placeholders); + return buildDepsGraph(ast); +} + +describe('VariableDeclaration', () => { + it('Identifier', () => { + const graph = _buildGraph` + const a = 42; + `; + + const deps = graph.getDependenciesByBinding('0:a'); + expect(deps).toMatchObject([ + { + type: 'VariableDeclarator', + }, + { + type: 'NumericLiteral', + value: 42, + }, + ]); + + expect(graph.findDependencies({ name: 'a' })).toContainEqual(deps[0]); + }); +}); + +describe('scopes', () => { + it('BlockStatement', () => { + const graph = _buildGraph` + let a = 42; + { + const a = "21"; + } + + a = 21; + `; + + const deps0 = graph.getDependenciesByBinding('0:a'); + expect(deps0).toMatchObject([ + { + type: 'VariableDeclarator', + }, + { + type: 'NumericLiteral', + value: 42, + }, + { + type: 'Identifier', + start: 4, + }, + { + type: 'Identifier', + start: 35, + }, + { + type: 'AssignmentExpression', + }, + { + type: 'NumericLiteral', + value: 21, + }, + ]); + + const deps1 = graph.getDependenciesByBinding('1:a'); + expect(deps1).toMatchObject([ + { + type: 'VariableDeclarator', + }, + { + type: 'StringLiteral', + value: '21', + }, + ]); + + expect(graph.findDependencies({ name: 'a' })).toHaveLength(8); + }); + + it('Function', () => { + const graph = _buildGraph` + const a = (arg1, arg2, arg3) => arg1 + arg2 + arg3; + `; + + const aDeps = graph.getDependenciesByBinding('0:a'); + expect(aDeps).toMatchObject([ + { + type: 'VariableDeclarator', + }, + { + type: 'ArrowFunctionExpression', + }, + ]); + + expect(graph.getDependenciesByBinding('1:arg1')).toHaveLength(3); + expect(graph.getDependentsByBinding('1:arg1')).toMatchObject([ + { + // arg1 in the binary expression + type: 'Identifier', + name: 'arg1', + start: 32, + }, + { + // arg1 + arg2 + type: 'BinaryExpression', + right: { + name: 'arg2', + }, + }, + { + // (arg1 + arg2) + arg3, because of it is the whole function body + type: 'BinaryExpression', + right: { + name: 'arg3', + }, + }, + ]); + + expect(graph.getDependenciesByBinding('1:arg2')).toMatchObject([ + { + type: 'ArrowFunctionExpression', + }, + { + type: 'Identifier', + name: 'arg2', + start: 17, + }, + { + type: 'BinaryExpression', + start: 32, + }, + ]); + + expect(graph.getDependenciesByBinding('1:arg3')).toMatchObject([ + { + type: 'ArrowFunctionExpression', + }, + { + type: 'Identifier', + name: 'arg3', + start: 23, + }, + { + type: 'BinaryExpression', + start: 32, + }, + ]); + }); +}); + +describe('AssignmentExpression', () => { + it('Identifier', () => { + const graph = _buildGraph` + let a = 42; + a = 24; + `; + + const deps = graph.getDependenciesByBinding('0:a'); + expect(deps).toMatchObject([ + { + type: 'VariableDeclarator', + }, + { + type: 'NumericLiteral', + value: 42, + }, + { + type: 'Identifier', + name: 'a', + start: 4, + }, + { + type: 'Identifier', + name: 'a', + start: 12, + }, + { + type: 'AssignmentExpression', + }, + { + type: 'NumericLiteral', + value: 24, + }, + ]); + + expect(graph.findDependents({ value: 42 })).toHaveLength(1); + expect(graph.findDependents({ value: 24 })).toHaveLength(1); + }); + + it('MemberExpression', () => { + const graph = _buildGraph` + const a = {}; + a.foo.bar = 42; + `; + + expect(graph.getDependenciesByBinding('0:a')).toMatchObject([ + { + type: 'VariableDeclarator', + }, + { + type: 'ObjectExpression', + properties: [], + }, + { + type: 'Identifier', + name: 'a', + start: 6, + }, + { + type: 'Identifier', + name: 'a', + start: 14, + }, + { + type: 'MemberExpression', + property: { + name: 'foo', + }, + }, + ]); + + expect(graph.findDependents({ value: 42 })).toMatchObject([ + { + type: 'MemberExpression', + property: { + name: 'bar', + }, + }, + ]); + }); +}); + +it('SequenceExpression', () => { + const graph = _buildGraph` + const color1 = 'blue'; + let local = ''; + const color2 = (true, local = color1, () => local); + `; + + const seqDeps = graph.findDependencies({ + type: 'SequenceExpression', + }); + expect(seqDeps).toMatchObject([ + { + type: 'ArrowFunctionExpression', + }, + { + id: { + name: 'color2', + }, + type: 'VariableDeclarator', + }, + ]); + + const fnDeps = graph.findDependencies({ + type: 'ArrowFunctionExpression', + }); + expect(fnDeps).toMatchObject([ + { + body: { + name: 'local', + type: 'Identifier', + }, + + type: 'ArrowFunctionExpression', + }, + { + name: 'local', + type: 'Identifier', + }, + { + type: 'SequenceExpression', + }, + ]); + + const localDeps = graph.getDependenciesByBinding('0:local'); + expect(localDeps).toMatchObject([ + { + type: 'VariableDeclarator', + }, + { + type: 'StringLiteral', + value: '', + }, + { + type: 'Identifier', + name: 'local', + start: 27, + }, + { + type: 'Identifier', + name: 'local', + start: 61, + }, + { + type: 'AssignmentExpression', + }, + { + type: 'Identifier', + name: 'color1', + }, + { + type: 'Identifier', + name: 'local', + start: 27, + }, + { + type: 'ArrowFunctionExpression', + }, + ]); + + const bool = { type: 'BooleanLiteral' }; + expect(graph.findDependents(bool)).toHaveLength(0); + expect(graph.findDependencies(bool)).toHaveLength(1); +}); + +it('MemberExpression', () => { + const graph = _buildGraph` + const key = 'blue'; + const obj = { blue: '#00F' }; + const blue = obj[key]; + `; + + const memberExprDeps = graph.findDependencies({ + type: 'MemberExpression', + }); + + expect(memberExprDeps).toMatchObject([ + { + type: 'Identifier', + name: 'obj', + }, + { + type: 'Identifier', + name: 'key', + }, + { + type: 'VariableDeclarator', + id: { + name: 'blue', + }, + }, + ]); +}); diff --git a/@linaria/packages/babel/__tests__/dynamic-import-noop.test.js b/@linaria/packages/babel/__tests__/dynamic-import-noop.test.js new file mode 100644 index 0000000..04f0452 --- /dev/null +++ b/@linaria/packages/babel/__tests__/dynamic-import-noop.test.js @@ -0,0 +1,15 @@ +import { transformAsync } from '@babel/core'; + +it('replaces dynamic imports with a noop', async () => { + const { code } = await transformAsync( + `import('./foo').then(foo => foo.init())`, + { + plugins: [require.resolve('../src/dynamic-import-noop')], + filename: 'source.js', + configFile: false, + babelrc: false, + } + ); + + expect(code).toMatchSnapshot(); +}); diff --git a/@linaria/packages/babel/__tests__/evaluators/__snapshots__/extractor.test.ts.snap b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/extractor.test.ts.snap new file mode 100644 index 0000000..7ee2296 --- /dev/null +++ b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/extractor.test.ts.snap @@ -0,0 +1,237 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`keeps objects as is 1`] = ` +"{ + const fill1 = (top = 0, left = 0, right = 0, bottom = 0) => ({ + position: 'absolute', + top, + right, + bottom, + left + }); + + { + const fill2 = (top = 0, left = 0, right = 0, bottom = 0) => { + return { + position: 'absolute', + top, + right, + bottom, + left + }; + }; + + { + exports.__linariaPreval = [fill1, fill2]; + } + } +}" +`; + +exports[`keeps only code which is related to \`a\` 1`] = ` +"{ + const { + whiteColor: color, + anotherColor + } = require('…'); + + { + const a = color || anotherColor; + { + exports.__linariaPreval = [a]; + } + } +}" +`; + +exports[`keeps only code which is related to \`anotherColor\` 1`] = ` +"{ + const { + whiteColor: color, + anotherColor + } = require('…'); + + { + exports.__linariaPreval = [anotherColor]; + } +}" +`; + +exports[`keeps only code which is related to \`color\` 1`] = ` +"{ + const { + whiteColor: color, + anotherColor + } = require('…'); + + { + exports.__linariaPreval = [color]; + } +}" +`; + +exports[`removes all 1`] = ` +"{ + exports.__linariaPreval = []; +}" +`; + +exports[`shakes assignment patterns 1`] = ` +"{ + const [identifier = 1] = [2]; + { + const [{ ...object + } = {}] = [{ + a: 1, + b: 2 + }]; + { + const [[...array] = []] = [[1, 2, 3, 4]]; + { + const obj = { + member: null + }; + { + exports.__linariaPreval = [identifier, object, array, obj]; + } + } + } + } +}" +`; + +exports[`shakes es5 exports 1`] = ` +"\\"use strict\\"; + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.redColor = 'red'; +Object.defineProperty(exports, \\"blueColor\\", { + enumerable: true, + get: function get() { + return 'blue'; + } +}); +Object.defineProperty(exports, \\"greenColor\\", { + enumerable: true, + get: function get() { + return 'green'; + } +});" +`; + +exports[`shakes exports 1`] = ` +"{ + var _ = require(\\"\\\\u2026\\"); + + { + const a = _.whiteColor; + { + exports.__linariaPreval = [a]; + } + } +}" +`; + +exports[`shakes imports 1`] = ` +"{ + var _ = _interopRequireWildcard(require(\\"\\\\u2026\\")); + + { + function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== \\"function\\") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function (nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); + } + + { + function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + + if (obj === null || typeof obj !== \\"object\\" && typeof obj !== \\"function\\") { + return { + default: obj + }; + } + + var cache = _getRequireWildcardCache(nodeInterop); + + if (cache && cache.has(obj)) { + return cache.get(obj); + } + + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + + for (var key in obj) { + if (key !== \\"default\\" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + + newObj.default = obj; + + if (cache) { + cache.set(obj, newObj); + } + + return newObj; + } + + { + exports.__linariaPreval = [_.whiteColor, _.default]; + } + } + } +}" +`; + +exports[`shakes sequence expression 1`] = ` +"{ + var _ = require(\\"\\\\u2026\\"); + + { + const color1 = () => 'blue'; + + { + let local = ''; + { + const color2 = () => local; + + { + exports.__linariaPreval = [color2]; + } + } + } + } +}" +`; + +exports[`should keep member expression key 1`] = ` +"{ + const key = 'blue'; + { + const obj = { + blue: '#00F' + }; + { + const blue = obj[key]; + { + exports.__linariaPreval = [blue]; + } + } + } +}" +`; diff --git a/@linaria/packages/babel/__tests__/evaluators/__snapshots__/preeval.test.ts.snap b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/preeval.test.ts.snap new file mode 100644 index 0000000..93660fa --- /dev/null +++ b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/preeval.test.ts.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`handles locally named import 1`] = ` +"import { styled as custom } from '@linaria/react'; +const Component = +/*linaria styled ci72cjv Component Component_ci72cjv*/ +custom.div\`\`;" +`; + +exports[`hoists exports 1`] = ` +"\\"use strict\\"; + +var _foo = require(\\"./foo\\"); + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +Object.defineProperty(exports, \\"foo\\", { + enumerable: true, + get: function get() { + return _foo.foo; + } +}); +Object.defineProperty(exports, \\"bar\\", { + enumerable: true, + get: function get() { + return _foo.bar; + } +});" +`; + +exports[`preserves classNames 1`] = ` +"import { styled } from '@linaria/react'; +const Component = +/*linaria styled ci72cjv Component Component_ci72cjv*/ +styled.div\`\`;" +`; + +exports[`replaces class component 1`] = ` +"import React from 'react'; + +function Component() { + return <></>; +}" +`; + +exports[`replaces constant 1`] = ` +"import React from 'react'; +const tag = <></>; + +const Component = props => tag;" +`; + +exports[`replaces functional component 1`] = ` +"import React from 'react'; + +const Component = () => { + return <></>; +};" +`; diff --git a/@linaria/packages/babel/__tests__/evaluators/__snapshots__/shaker.test.ts.snap b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/shaker.test.ts.snap new file mode 100644 index 0000000..92b9b9e --- /dev/null +++ b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/shaker.test.ts.snap @@ -0,0 +1,213 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`keeps objects as is 1`] = ` +"\\"use strict\\"; + +var fill1 = function fill1() { + var top = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + var left = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + var right = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + var bottom = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; + return { + position: 'absolute', + top: top, + right: right, + bottom: bottom, + left: left + }; +}; + +var fill2 = function fill2() { + var top = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + var left = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + var right = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + var bottom = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; + return { + position: 'absolute', + top: top, + right: right, + bottom: bottom, + left: left + }; +}; + +exports.__linariaPreval = [fill1, fill2];" +`; + +exports[`keeps only code which is related to \`a\` 1`] = ` +"\\"use strict\\"; + +var _require = require('…'), + color = _require.whiteColor, + anotherColor = _require.anotherColor; + +var a = color || anotherColor; +color.green = '#0f0'; +exports.__linariaPreval = [a];" +`; + +exports[`keeps only code which is related to \`anotherColor\` 1`] = ` +"\\"use strict\\"; + +var _require = require('…'), + anotherColor = _require.anotherColor; + +exports.__linariaPreval = [anotherColor];" +`; + +exports[`keeps only code which is related to \`color\` 1`] = ` +"\\"use strict\\"; + +var _require = require('…'), + color = _require.whiteColor; + +color.green = '#0f0'; +exports.__linariaPreval = [color];" +`; + +exports[`keeps only the last assignment of each exported variable 1`] = ` +"\\"use strict\\"; + +var bar = function bar() { + return 'hello world'; +}; + +exports.bar = bar; +var foo = exports.bar(); +exports.__linariaPreval = [foo];" +`; + +exports[`keeps reused exports 1`] = ` +"\\"use strict\\"; + +var bar = function bar() { + return 'hello world'; +}; + +exports.bar = bar; +var foo = exports.bar(); +exports.__linariaPreval = [foo];" +`; + +exports[`removes all 1`] = ` +"\\"use strict\\"; + +exports.__linariaPreval = [];" +`; + +exports[`shakes assignment patterns 1`] = ` +"\\"use strict\\"; + +var _interopRequireDefault = require(\\"@babel/runtime/helpers/interopRequireDefault\\"); + +var _toArray2 = _interopRequireDefault(require(\\"@babel/runtime/helpers/toArray\\")); + +var _extends2 = _interopRequireDefault(require(\\"@babel/runtime/helpers/extends\\")); + +var _ = 2, + identifier = _ === void 0 ? 1 : _; +var _a$b = { + a: 1, + b: 2 +}; +_a$b = _a$b === void 0 ? {} : _a$b; +var object = (_extends2.default)({}, _a$b); +var _ref = [1, 2, 3, 4]; +_ref = _ref === void 0 ? [] : _ref; + +var _ref2 = (_toArray2.default)(_ref), + array = _ref2.slice(0); + +var obj = { + member: null +}; +var _2 = 1; +obj.member = _2 === void 0 ? 42 : _2; +exports.__linariaPreval = [identifier, object, array, obj];" +`; + +exports[`shakes es5 exports 1`] = ` +"\\"use strict\\"; + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.redColor = 'red'; +exports['yellowColor'] = 'yellow'; +Object.defineProperty(exports, \\"greenColor\\", { + enumerable: true, + get: function get() { + return 'green'; + } +});" +`; + +exports[`shakes exports 1`] = ` +"\\"use strict\\"; + +var _ = require(\\"\\\\u2026\\"); + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +var a = _.whiteColor; +exports.__linariaPreval = [a];" +`; + +exports[`shakes for-in statements 1`] = ` +"\\"use strict\\"; + +var obj1 = { + a: 1, + b: 2 +}; +var obj2 = {}; + +for (var key in obj1) { + obj2[key] = obj1[key]; +} + +exports.__linariaPreval = [obj2];" +`; + +exports[`shakes imports 1`] = ` +"\\"use strict\\"; + +var _typeof = require(\\"@babel/runtime/helpers/typeof\\"); + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); + +var _ = _interopRequireWildcard(require(\\"\\\\u2026\\")); + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== \\"function\\") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== \\"object\\" && typeof obj !== \\"function\\") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== \\"default\\" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +exports.__linariaPreval = [_.whiteColor, _.default];" +`; + +exports[`shakes sequence expression 1`] = ` +"\\"use strict\\"; + +var color1 = (function () { + return 'blue'; +}); +var local = ''; +var color2 = (local = color1(), function () { + return local; +}); +exports.__linariaPreval = [color2];" +`; + +exports[`should keep member expression key 1`] = ` +"\\"use strict\\"; + +var key = 'blue'; +var obj = { + blue: '#00F' +}; +var blue = obj[key]; +exports.__linariaPreval = [blue];" +`; diff --git a/@linaria/packages/babel/__tests__/evaluators/extractor.test.ts b/@linaria/packages/babel/__tests__/evaluators/extractor.test.ts new file mode 100644 index 0000000..b764a25 --- /dev/null +++ b/@linaria/packages/babel/__tests__/evaluators/extractor.test.ts @@ -0,0 +1,214 @@ +import path from 'path'; +import dedent from 'dedent'; +import type { TransformOptions } from '@babel/core'; +import exctract from '../../../extractor/src'; + +function getFileName() { + return path.resolve(__dirname, `../__fixtures__/test.js`); +} + +function _shake(opts?: TransformOptions, only: string[] = ['__linariaPreval']) { + return ( + literal: TemplateStringsArray, + ...placeholders: string[] + ): [string, Map<string, string[]>] => { + const code = dedent(literal, ...placeholders); + const [shaken, deps] = exctract( + getFileName(), + { + babelOptions: opts || {}, + displayName: true, + evaluate: true, + rules: [ + { + action: require('../../../extractor/src').default, + }, + { + test: /\/node_modules\//, + action: 'ignore', + }, + ], + }, + code, + only + ); + + return [shaken, deps!]; + }; +} + +it('removes all', () => { + const [shaken] = _shake()` + const { whiteColor: color, anotherColor } = require('…'); + const a = color || anotherColor; + color.green = '#0f0'; + + exports.__linariaPreval = []; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps only code which is related to `color`', () => { + const [shaken] = _shake()` + const { whiteColor: color, anotherColor } = require('…'); + const wrap = ''; + const a = color || anotherColor; + color.green = '#0f0'; + module.exports = { color, anotherColor }; + exports.__linariaPreval = [color]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps only code which is related to `anotherColor`', () => { + const [shaken] = _shake()` + const { whiteColor: color, anotherColor } = require('…'); + const a = color || anotherColor; + color.green = '#0f0'; + exports.__linariaPreval = [anotherColor]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps only code which is related to `a`', () => { + const [shaken] = _shake()` + const { whiteColor: color, anotherColor } = require('…'); + const a = color || anotherColor; + color.green = '#0f0'; + exports.__linariaPreval = [a]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes imports', () => { + const [shaken] = _shake()` + import { unrelatedImport } from '…'; + import { whiteColor as color, anotherColor } from '…'; + import defaultColor from '…'; + import anotherDefaultColor from '…'; + import '…'; + require('…'); + export default color; + exports.__linariaPreval = [color, defaultColor]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('should keep member expression key', () => { + const [shaken] = _shake()` + const key = 'blue'; + const obj = { blue: '#00F' }; + const blue = obj[key]; + exports.__linariaPreval = [blue]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes exports', () => { + const [shaken] = _shake()` + import { whiteColor as color, anotherColor } from '…'; + export const a = color; + export { redColor } from "…"; + export { anotherColor }; + exports.__linariaPreval = [a]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes es5 exports', () => { + const [shaken] = _shake(undefined, ['redColor', 'greenColor'])` + "use strict"; + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.redColor = 'red'; + Object.defineProperty(exports, "blueColor", { + enumerable: true, + get: function get() { + return 'blue'; + } + }); + Object.defineProperty(exports, "greenColor", { + enumerable: true, + get: function get() { + return 'green'; + } + }); + `; + + expect(shaken).toMatchSnapshot(); +}); + +// TODO: this test will be disabled until the shaker is fully implemented +// eslint-disable-next-line jest/no-disabled-tests +it.skip('should throw away any side effects', () => { + const [shaken] = _shake()` + const objects = { key: { fontSize: 12 } }; + const foo = (k) => { + const obj = objects[k]; + console.log('side effect'); + return obj; + }; + exports.__linariaPreval = [foo]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps objects as is', () => { + const [shaken] = _shake()` + const fill1 = (top = 0, left = 0, right = 0, bottom = 0) => ({ + position: 'absolute', + top, + right, + bottom, + left, + }); + + const fill2 = (top = 0, left = 0, right = 0, bottom = 0) => { + return { + position: 'absolute', + top, + right, + bottom, + left, + }; + }; + + exports.__linariaPreval = [fill1, fill2]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes sequence expression', () => { + const [shaken] = _shake()` + import { external } from '…'; + const color1 = (external, () => 'blue'); + let local = ''; + const color2 = (local = color1(), () => local); + exports.__linariaPreval = [color2]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes assignment patterns', () => { + const [shaken] = _shake()` + const [identifier = 1] = [2]; + const [{...object} = {}] = [{ a: 1, b: 2 }]; + const [[...array] = []] = [[1,2,3,4]]; + const obj = { member: null }; + ([obj.member = 42] = [1]); + exports.__linariaPreval = [identifier, object, array, obj]; + `; + + expect(shaken).toMatchSnapshot(); +}); diff --git a/@linaria/packages/babel/__tests__/evaluators/preeval.test.ts b/@linaria/packages/babel/__tests__/evaluators/preeval.test.ts new file mode 100644 index 0000000..307abd7 --- /dev/null +++ b/@linaria/packages/babel/__tests__/evaluators/preeval.test.ts @@ -0,0 +1,121 @@ +import { join } from 'path'; +import { transformAsync } from '@babel/core'; +import dedent from 'dedent'; +import serializer from '../../__utils__/linaria-snapshot-serializer'; +import type { StrictOptions } from '../../src'; + +expect.addSnapshotSerializer(serializer); + +const options: Partial<StrictOptions> = { + displayName: true, + evaluate: true, +}; + +const transpile = async (input: string) => + (await transformAsync(input, { + babelrc: false, + presets: [[require.resolve('@linaria/preeval'), options]], + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-syntax-jsx', + ], + filename: join(__dirname, 'app/index.js'), + configFile: false, + }))!; + +it('preserves classNames', async () => { + const { code } = await transpile( + dedent` + import { styled } from '@linaria/react'; + + const Component = styled.div\`\`; + ` + ); + + expect(code).toMatchSnapshot(); +}); + +it('handles locally named import', async () => { + const { code } = await transpile( + dedent` + import { styled as custom } from '@linaria/react'; + + const Component = custom.div\`\`; + ` + ); + + expect(code).toMatchSnapshot(); +}); + +it('replaces functional component', async () => { + const div = '<div>{props.children}</div>'; + const { code } = await transpile( + dedent` + import React from 'react'; + + const Component = (props) => ${div}; + ` + ); + + expect(code).toMatchSnapshot(); +}); + +it('replaces class component', async () => { + const div = '<div>{props.children}</div>'; + const { code } = await transpile( + dedent` + import React from 'react'; + + class Component extends React.PureComponent { + render() { + return ${div}; + } + } + ` + ); + + expect(code).toMatchSnapshot(); +}); + +it('replaces constant', async () => { + const div = '<div>{props.children}</div>'; + const { code } = await transpile( + dedent` + import React from 'react'; + + const tag = ${div}; + + const Component = (props) => tag; + ` + ); + + expect(code).toMatchSnapshot(); +}); + +it('hoists exports', async () => { + const { code } = await transpile( + dedent` + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + Object.defineProperty(exports, "foo", { + enumerable: true, + get: function get() { + return _foo.foo; + } + }); + Object.defineProperty(exports, "bar", { + enumerable: true, + get: function get() { + return _foo.bar; + } + }); + + var _foo = require("./foo"); + ` + ); + + expect(code).toMatchSnapshot(); +}); diff --git a/@linaria/packages/babel/__tests__/evaluators/shaker.test.ts b/@linaria/packages/babel/__tests__/evaluators/shaker.test.ts new file mode 100644 index 0000000..1208065 --- /dev/null +++ b/@linaria/packages/babel/__tests__/evaluators/shaker.test.ts @@ -0,0 +1,259 @@ +import path from 'path'; +import dedent from 'dedent'; +import type { TransformOptions } from '@babel/core'; +import shake from '../../../shaker/src'; + +function getFileName() { + return path.resolve(__dirname, `../__fixtures__/test.js`); +} + +function _shake(opts?: TransformOptions, only: string[] = ['__linariaPreval']) { + return ( + literal: TemplateStringsArray, + ...placeholders: string[] + ): [string, Map<string, string[]>] => { + const code = dedent(literal, ...placeholders); + const [shaken, deps] = shake( + getFileName(), + { + babelOptions: opts || {}, + displayName: true, + evaluate: true, + rules: [ + { + action: require('../../../extractor/src').default, + }, + { + test: /\/node_modules\//, + action: 'ignore', + }, + ], + }, + code, + only + ); + + return [shaken, deps!]; + }; +} + +it('removes all', () => { + const [shaken] = _shake()` + const { whiteColor: color, anotherColor } = require('…'); + const a = color || anotherColor; + color.green = '#0f0'; + + exports.__linariaPreval = []; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps only code which is related to `color`', () => { + const [shaken] = _shake()` + const { whiteColor: color, anotherColor } = require('…'); + const wrap = ''; + const a = color || anotherColor; + color.green = '#0f0'; + module.exports = { color, anotherColor }; + exports.__linariaPreval = [color]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps only code which is related to `anotherColor`', () => { + const [shaken] = _shake()` + const { whiteColor: color, anotherColor } = require('…'); + const a = color || anotherColor; + color.green = '#0f0'; + exports.__linariaPreval = [anotherColor]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps only code which is related to `a`', () => { + const [shaken] = _shake()` + const { whiteColor: color, anotherColor } = require('…'); + const a = color || anotherColor; + color.green = '#0f0'; + exports.__linariaPreval = [a]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes imports', () => { + const [shaken] = _shake()` + import { unrelatedImport } from '…'; + import { whiteColor as color, anotherColor } from '…'; + import defaultColor from '…'; + import anotherDefaultColor from '…'; + import '…'; + require('…'); + export default color; + exports.__linariaPreval = [color, defaultColor]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('should keep member expression key', () => { + const [shaken] = _shake()` + const key = 'blue'; + const obj = { blue: '#00F' }; + const blue = obj[key]; + exports.__linariaPreval = [blue]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes exports', () => { + const [shaken] = _shake()` + import { whiteColor as color, anotherColor } from '…'; + export const a = color; + export { redColor } from "…"; + export { anotherColor }; + exports.__linariaPreval = [a]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes es5 exports', () => { + const [shaken] = _shake(undefined, ['redColor', 'greenColor', 'yellowColor'])` + "use strict"; + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.redColor = 'red'; + exports['yellowColor'] = 'yellow'; + exports['pinkColor'] = 'pink'; + Object.defineProperty(exports, "blueColor", { + enumerable: true, + get: function get() { + return 'blue'; + } + }); + Object.defineProperty(exports, "greenColor", { + enumerable: true, + get: function get() { + return 'green'; + } + }); + `; + + expect(shaken).toMatchSnapshot(); +}); + +// TODO: this test will be disabled until the shaker is fully implemented +// eslint-disable-next-line jest/no-disabled-tests +it.skip('should throw away any side effects', () => { + const [shaken] = _shake()` + const objects = { key: { fontSize: 12 } }; + const foo = (k) => { + const obj = objects[k]; + console.log('side effect'); + return obj; + }; + exports.__linariaPreval = [foo]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps objects as is', () => { + const [shaken] = _shake()` + const fill1 = (top = 0, left = 0, right = 0, bottom = 0) => ({ + position: 'absolute', + top, + right, + bottom, + left, + }); + + const fill2 = (top = 0, left = 0, right = 0, bottom = 0) => { + return { + position: 'absolute', + top, + right, + bottom, + left, + }; + }; + + exports.__linariaPreval = [fill1, fill2]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes sequence expression', () => { + const [shaken] = _shake()` + import { external } from '…'; + const color1 = (external, () => 'blue'); + let local = ''; + const color2 = (local = color1(), () => local); + exports.__linariaPreval = [color2]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes assignment patterns', () => { + const [shaken] = _shake()` + const [identifier = 1] = [2]; + const [{...object} = {}] = [{ a: 1, b: 2 }]; + const [[...array] = []] = [[1,2,3,4]]; + const obj = { member: null }; + ([obj.member = 42] = [1]); + exports.__linariaPreval = [identifier, object, array, obj]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('shakes for-in statements', () => { + const [shaken] = _shake()` + const obj1 = { a: 1, b: 2 }; + const obj2 = {}; + for (const key in obj1) { + obj2[key] = obj1[key]; + } + exports.__linariaPreval = [obj2]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps reused exports', () => { + const [shaken] = _shake()` + const bar = function() { + return 'hello world'; + }; + exports.bar = bar; + + const foo = exports.bar(); + exports.__linariaPreval = [foo]; + `; + + expect(shaken).toMatchSnapshot(); +}); + +it('keeps only the last assignment of each exported variable', () => { + const [shaken] = _shake()` + const bar = function() { + return 'hello world'; + }; + + exports.bar = "bar"; + exports.bar = bar; + + const foo = exports.bar(); + exports.__linariaPreval = [foo]; + `; + + expect(shaken).toMatchSnapshot(); +}); diff --git a/@linaria/packages/babel/__tests__/module.test.ts b/@linaria/packages/babel/__tests__/module.test.ts new file mode 100644 index 0000000..1012b93 --- /dev/null +++ b/@linaria/packages/babel/__tests__/module.test.ts @@ -0,0 +1,320 @@ +import path from 'path'; +import dedent from 'dedent'; +import * as babel from '@babel/core'; +import { Module } from '../src'; +import type { Evaluator, StrictOptions } from '../src'; + +beforeEach(() => Module.invalidate()); + +const evaluator: Evaluator = (filename, options, text) => { + const { code } = babel.transformSync(text, { + filename: filename, + })!; + return [code!, null]; +}; + +function getFileName() { + return path.resolve(__dirname, `../__fixtures__/test.js`); +} + +const options: StrictOptions = { + displayName: false, + evaluate: true, + rules: [ + { + action: evaluator, + }, + { + test: /\/node_modules\//, + action: 'ignore', + }, + ], + babelOptions: {}, +}; + +beforeEach(() => Module.invalidateEvalCache()); + +it('creates module for JS files', () => { + const filename = '/foo/bar/test.js'; + const mod = new Module(filename, options); + + mod.evaluate('module.exports = () => 42'); + + expect(mod.exports()).toBe(42); + expect(mod.id).toBe(filename); + expect(mod.filename).toBe(filename); +}); + +it('requires JS files', () => { + const mod = new Module(getFileName(), options); + + mod.evaluate(dedent` + const answer = require('./sample-script'); + + module.exports = 'The answer is ' + answer; + `); + + expect(mod.exports).toBe('The answer is 42'); +}); + +it('requires JSON files', () => { + const mod = new Module(getFileName(), options); + + mod.evaluate(dedent` + const data = require('./sample-data.json'); + + module.exports = 'Our saviour, ' + data.name; + `); + + expect(mod.exports).toBe('Our saviour, Luke Skywalker'); +}); + +it('imports JS files', () => { + const mod = new Module(getFileName(), options); + + mod.evaluate(dedent` + import answer from './sample-script'; + + export const result = 'The answer is ' + answer; + `); + + expect(mod.exports.result).toBe('The answer is 42'); +}); + +it('imports TypeScript files', () => { + const mod = new Module( + path.resolve(__dirname, '../__fixtures__/test.ts'), + options + ); + + mod.evaluate(dedent` + import answer from './sample-typescript'; + + export const result = 'The answer is ' + answer; + `); + + expect(mod.exports.result).toBe('The answer is 27'); +}); + +it('imports JSON files', () => { + const mod = new Module(getFileName(), options); + + mod.evaluate(dedent` + import data from './sample-data.json'; + + const result = 'Our saviour, ' + data.name; + + export default result; + `); + + expect(mod.exports.default).toBe('Our saviour, Luke Skywalker'); +}); + +it('returns module from the cache', () => { + /* eslint-disable no-self-compare */ + + const filename = getFileName(); + const mod = new Module(filename, options); + const id = './sample-data.json'; + + expect(mod.require(id) === mod.require(id)).toBe(true); + + expect( + new Module(filename, options).require(id) === + new Module(filename, options).require(id) + ).toBe(true); +}); + +it('clears modules from the cache', () => { + const filename = getFileName(); + const id = './sample-data.json'; + + const result = new Module(filename, options).require(id); + + expect(result === new Module(filename, options).require(id)).toBe(true); + + Module.invalidate(); + + expect(result === new Module(filename, options).require(id)).toBe(false); +}); + +it('exports the path for non JS/JSON files', () => { + const mod = new Module(getFileName(), options); + + expect(mod.require('./sample-asset.png')).toBe('./sample-asset.png'); +}); + +it('returns module when requiring mocked builtin node modules', () => { + const mod = new Module(getFileName(), options); + + expect(mod.require('path')).toBe(require('path')); +}); + +it('returns null when requiring empty builtin node modules', () => { + const mod = new Module(getFileName(), options); + + expect(mod.require('fs')).toBe(null); +}); + +it('throws when requiring unmocked builtin node modules', () => { + const mod = new Module(getFileName(), options); + + expect(() => mod.require('perf_hooks')).toThrow( + 'Unable to import "perf_hooks". Importing Node builtins is not supported in the sandbox.' + ); +}); + +it('has access to the global object', () => { + const mod = new Module(getFileName(), options); + + expect(() => + mod.evaluate(dedent` + new global.Set(); + `) + ).not.toThrow(); +}); + +it("doesn't have access to the process object", () => { + const mod = new Module(getFileName(), options); + + expect(() => + mod.evaluate(dedent` + module.exports = process.abort(); + `) + ).toThrow('process.abort is not a function'); +}); + +it('has access to NODE_ENV', () => { + const mod = new Module(getFileName(), options); + + mod.evaluate(dedent` + module.exports = process.env.NODE_ENV; + `); + + expect(mod.exports).toBe(process.env.NODE_ENV); +}); + +it('has require.resolve available', () => { + const mod = new Module(getFileName(), options); + + mod.evaluate(dedent` + module.exports = require.resolve('./sample-script'); + `); + + expect(mod.exports).toBe( + path.resolve(path.dirname(mod.filename), 'sample-script.js') + ); +}); + +it('has require.ensure available', () => { + const mod = new Module(getFileName(), options); + + expect(() => + mod.evaluate(dedent` + require.ensure(['./sample-script']); + `) + ).not.toThrow(); +}); + +it('has __filename available', () => { + const mod = new Module(getFileName(), options); + + mod.evaluate(dedent` + module.exports = __filename; + `); + + expect(mod.exports).toBe(mod.filename); +}); + +it('has __dirname available', () => { + const mod = new Module(getFileName(), options); + + mod.evaluate(dedent` + module.exports = __dirname; + `); + + expect(mod.exports).toBe(path.dirname(mod.filename)); +}); + +it('has setTimeout, clearTimeout available', () => { + const mod = new Module(getFileName(), options); + + expect(() => + mod.evaluate(dedent` + const x = setTimeout(() => { + console.log('test'); + },0); + + clearTimeout(x); + `) + ).not.toThrow(); +}); + +it('has setInterval, clearInterval available', () => { + const mod = new Module(getFileName(), options); + + expect(() => + mod.evaluate(dedent` + const x = setInterval(() => { + console.log('test'); + }, 1000); + + clearInterval(x); + `) + ).not.toThrow(); +}); + +it('has setImmediate, clearImmediate available', () => { + const mod = new Module(getFileName(), options); + + expect(() => + mod.evaluate(dedent` + const x = setImmediate(() => { + console.log('test'); + }); + + clearImmediate(x); + `) + ).not.toThrow(); +}); + +it('has global objects available without referencing global', () => { + const mod = new Module(getFileName(), options); + + expect(() => + mod.evaluate(dedent` + const x = new Set(); + `) + ).not.toThrow(); +}); + +it('changes resolve behaviour on overriding _resolveFilename', () => { + const originalResolveFilename = Module._resolveFilename; + + Module._resolveFilename = (id) => (id === 'foo' ? 'bar' : id); + + const mod = new Module(getFileName(), options); + + mod.evaluate(dedent` + module.exports = [ + require.resolve('foo'), + require.resolve('test'), + ]; + `); + + // Restore old behavior + Module._resolveFilename = originalResolveFilename; + + expect(mod.exports).toEqual(['bar', 'test']); +}); + +it('correctly processes export declarations in strict mode', () => { + const filename = '/foo/bar/test.js'; + const mod = new Module(filename, options); + + mod.evaluate('"use strict"; exports = module.exports = () => 42'); + + expect(mod.exports()).toBe(42); + expect(mod.id).toBe(filename); + expect(mod.filename).toBe(filename); +}); diff --git a/@linaria/packages/babel/__tests__/transform.test.ts b/@linaria/packages/babel/__tests__/transform.test.ts new file mode 100644 index 0000000..c830ad9 --- /dev/null +++ b/@linaria/packages/babel/__tests__/transform.test.ts @@ -0,0 +1,237 @@ +/* eslint-disable no-template-curly-in-string */ + +import path from 'path'; +import dedent from 'dedent'; +import transform, { transformUrl } from '../src/transform'; +import evaluator from '../../extractor/src'; + +const outputFilename = './.linaria-cache/test.css'; + +const rules = [ + { + test: () => true, + action: evaluator, + }, +]; + +describe('transformUrl', () => { + type TransformUrlArgs = Parameters<typeof transformUrl>; + const dataset: Record<string, TransformUrlArgs> = { + '../assets/test.jpg': [ + './assets/test.jpg', + './.linaria-cache/test.css', + './test.js', + ], + '../a/b/test.jpg': [ + '../a/b/test.jpg', + './.linaria-cache/test.css', + './a/test.js', + ], + }; + + it('should work with posix paths', () => { + for (const result of Object.keys(dataset)) { + expect(transformUrl(...dataset[result])).toBe(result); + } + }); + + it('should work with win32 paths', () => { + const toWin32 = (p: string) => p.split(path.posix.sep).join(path.win32.sep); + const win32Dataset = Object.keys(dataset).reduce( + (acc, key) => ({ + ...acc, + [key]: [ + dataset[key][0], + toWin32(dataset[key][1]), + toWin32(dataset[key][2]), + path.win32, + ] as TransformUrlArgs, + }), + {} as Record<string, TransformUrlArgs> + ); + + for (const result of Object.keys(win32Dataset)) { + expect(transformUrl(...win32Dataset[result])).toBe(result); + } + }); +}); + +it('rewrites a relative path in url() declarations', async () => { + const { cssText } = await transform( + dedent` + import { css } from '@linaria/core'; + + export const title = css\` + background-image: url(./assets/test.jpg); + background-image: url("./assets/test.jpg"); + background-image: url('./assets/test.jpg'); + \`; + `, + { + filename: './test.js', + outputFilename: './.linaria-cache/test.css', + pluginOptions: { + rules, + }, + } + ); + + expect(cssText).toMatchSnapshot(); +}); + +it('rewrites multiple relative paths in url() declarations', async () => { + const { cssText } = await transform( + dedent` + import { css } from '@linaria/core'; + + export const title = css\` + @font-face { + font-family: Test; + src: url(./assets/font.woff2) format("woff2"), url(./assets/font.woff) format("woff"); + } + \`; + `, + { + filename: './test.js', + outputFilename, + pluginOptions: { + rules, + }, + } + ); + + expect(cssText).toMatchSnapshot(); +}); + +it("doesn't rewrite an absolute path in url() declarations", async () => { + const { cssText } = await transform( + dedent` + import { css } from '@linaria/core'; + + export const title = css\` + background-image: url(/assets/test.jpg); + \`; + `, + { + filename: './test.js', + outputFilename, + pluginOptions: { + rules, + }, + } + ); + + expect(cssText).toMatchSnapshot(); +}); + +it('respects passed babel options', async () => { + expect.assertions(2); + + expect(() => + transform( + dedent` + import { css } from '@linaria/core'; + + export const error = <jsx />; + `, + { + filename: './test.js', + outputFilename, + pluginOptions: { + rules, + babelOptions: { + babelrc: false, + configFile: false, + presets: [['@babel/preset-env', { loose: true }]], + }, + }, + } + ) + ).toThrow( + /Support for the experimental syntax 'jsx' isn't currently enabled/ + ); + + expect(() => + transform( + dedent` + import { css } from '@linaria/core'; + + export const error = <jsx />; + export const title = css\` + background-image: url(/assets/test.jpg); + \`; + `, + { + filename: './test.js', + outputFilename, + pluginOptions: { + rules, + babelOptions: { + babelrc: false, + configFile: false, + presets: [ + ['@babel/preset-env', { loose: true }], + '@babel/preset-react', + ], + }, + }, + } + ) + ).not.toThrow('Unexpected token'); +}); + +it("doesn't throw due to duplicate preset", async () => { + expect.assertions(1); + + expect(() => + transform( + dedent` + import { styled } from '@linaria/react'; + + const Title = styled.h1\` color: blue; \`; + + const Article = styled.article\` + ${'${Title}'} { + font-size: 16px; + } + \`; + `, + { + filename: './test.js', + outputFilename, + pluginOptions: { + rules, + babelOptions: { + babelrc: false, + configFile: false, + presets: [require.resolve('../src')], + plugins: [ + require.resolve('@babel/plugin-transform-modules-commonjs'), + ], + }, + }, + } + ) + ).not.toThrow('Duplicate plugin/preset detected'); +}); + +it('should return transformed code even when file only contains unused linaria code', async () => { + const { code } = await transform( + dedent` + import { css } from '@linaria/core'; + + const title = css\` + color: red; + \`; + `, + { + filename: './test.js', + outputFilename, + pluginOptions: { + rules, + }, + } + ); + + expect(code).not.toContain('css`'); +}); |