From 354e5d3ffe11f1c95217728d7cc3415060a442ae Mon Sep 17 00:00:00 2001 From: Vladimir kalmykov Date: Sun, 2 Sep 2018 23:03:28 +0300 Subject: [PATCH] #88: keepImport feature --- README.md | 34 +++++ src/plugin.js | 116 ++++++++++++++---- test/fixtures/keep.import.expected.js | 9 ++ test/fixtures/keep.import.no.name.expected.js | 3 + test/fixtures/keep.require.expected.js | 7 ++ .../keep.require.in.expression.expected.js | 11 ++ .../fixtures/keep.require.no.name.expected.js | 3 + test/fixtures/require.in.expression.js | 4 + test/fixtures/require.no.name.js | 1 + test/helpers.js | 5 +- test/plugin.js | 110 +++++++++++++++++ 11 files changed, 274 insertions(+), 29 deletions(-) create mode 100644 test/fixtures/keep.import.expected.js create mode 100644 test/fixtures/keep.import.no.name.expected.js create mode 100644 test/fixtures/keep.require.expected.js create mode 100644 test/fixtures/keep.require.in.expression.expected.js create mode 100644 test/fixtures/keep.require.no.name.expected.js create mode 100644 test/fixtures/require.in.expression.js create mode 100644 test/fixtures/require.no.name.js diff --git a/README.md b/README.md index cfc1d43..e075d6e 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,40 @@ By default we look for `.css` files, but you can also specify the extensions we } ``` +## Keeping import + +By default, original import declaration in javascript code will be replaced with an object with styles. But you may keep import declarations by turning the option `keepImport` to `true`. + +```json +{ + "plugins": [ + ["transform-postcss", { + "config": "configuration/postcss.config.js", + "extensions": [".scss"], + "keepImport": true + }] + ] +} +``` + +In this case, you will still get the required objects with styles, but import declarations (or require expressions) will be kept above without any assignment expression. + +For example, this code: + +```js +import styles from './styles'; +``` + +```css +.example { color: cyan; } +``` + +Will be transformed to: + +```js +import './styles'; +var styles = {"example":"_example_amfqe_1"}; +``` ## Details diff --git a/src/plugin.js b/src/plugin.js index 4373338..5d9c407 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -27,6 +27,22 @@ const serverExcutable = join(__dirname, 'postcss-server.js'); let server; +/* eslint-disable id-length */ +const findExpressionStatementChild = (path: any, t: any): any => { + const parent = path.parentPath; + + if ( + t.isExpressionStatement(parent) || + t.isProgram(parent) || + t.isBlockStatement(parent) + ) { + return path; + } + + return findExpressionStatementChild(parent, t); +}; +/* eslint-enable id-length */ + const startServer = () => { server = spawn(nodeExecutable, [serverExcutable, socketPath, tmpPath], { env: process.env, // eslint-disable-line no-process-env @@ -96,32 +112,61 @@ export default function transformPostCSS({ types: t }: any): any { } const [{ value: stylesheetPath }] = args; - const { config, extensions } = this.opts; - const tokens = getStylesFromStylesheet( - stylesheetPath, - file, - config, - extensions - ); - - if (tokens !== undefined) { - const expression = path.findParent((test) => ( - test.isVariableDeclaration() || - test.isExpressionStatement() - )); - - expression.addComment( - 'trailing', ` @related-file ${stylesheetPath}`, true + const { config, extensions, keepImport } = this.opts; + + if (!t.isExpressionStatement(path.parent)) { + const tokens = getStylesFromStylesheet( + stylesheetPath, + file, + config, + extensions ); - path.replaceWith(t.objectExpression( - Object.keys(tokens).map( - (token) => t.objectProperty( - t.stringLiteral(token), - t.stringLiteral(tokens[token]) + if (tokens !== undefined) { + const finalExpression = t.objectExpression( + Object.keys(tokens).map( + (token) => t.objectProperty( + t.stringLiteral(token), + t.stringLiteral(tokens[token]) + ) ) - ) - )); + ); + + path.replaceWith(finalExpression); + + if (t.isProperty(path.parentPath)) { + path.parentPath.addComment( + 'trailing', ` @related-file ${stylesheetPath}`, true + ); + } + else { + + // Add comment + const expression = path.findParent((test) => ( + test.isVariableDeclaration() || + test.isExpressionStatement() + )); + + expression.addComment( + 'trailing', ` @related-file ${stylesheetPath}`, true + ); + } + + // Keeped `require` will be placed before closest expression + // statement child + if (keepImport) { + findExpressionStatementChild(path, t) + .insertBefore(t.expressionStatement( + t.callExpression( + t.identifier('require'), + [t.stringLiteral(stylesheetPath)] + ) + )); + } + } + } + else if (!keepImport) { + path.remove(); } }, ImportDeclaration(path: any, { file }: any) { @@ -131,7 +176,7 @@ export default function transformPostCSS({ types: t }: any): any { return; } - const { config, extensions } = this.opts; + const { config, extensions, keepImport } = this.opts; const tokens = getStylesFromStylesheet( stylesheetPath, file, @@ -153,9 +198,26 @@ export default function transformPostCSS({ types: t }: any): any { const variableDeclaration = t.VariableDeclaration('var', [t.VariableDeclarator(path.node.specifiers[0].local, styles)]); - /* eslint-enable new-cap */ - path.addComment('trailing', ` @related-file ${stylesheetPath}`, true); - path.replaceWith(variableDeclaration); + if (keepImport) { + path.replaceWithMultiple([ + t.importDeclaration([], t.stringLiteral(stylesheetPath)), + variableDeclaration, + ]); + + // Add comment directly to the variable declaration + variableDeclaration.trailingComments.push({ + type: 'CommentLine', + value: ` @related-file ${stylesheetPath}`, + }); + } + else { + path.replaceWith(variableDeclaration); + + // Add comment + path.addComment( + 'trailing', ` @related-file ${stylesheetPath}`, true + ); + } } }, }, diff --git a/test/fixtures/keep.import.expected.js b/test/fixtures/keep.import.expected.js new file mode 100644 index 0000000..6bac301 --- /dev/null +++ b/test/fixtures/keep.import.expected.js @@ -0,0 +1,9 @@ +"use strict"; + +require("./simple.css"); + +var styles = { + "simple": "_simple_jvai8_1" +}; // @related-file ./simple.css + +console.log(styles); diff --git a/test/fixtures/keep.import.no.name.expected.js b/test/fixtures/keep.import.no.name.expected.js new file mode 100644 index 0000000..e3b3220 --- /dev/null +++ b/test/fixtures/keep.import.no.name.expected.js @@ -0,0 +1,3 @@ +'use strict'; + +require('simple.css'); diff --git a/test/fixtures/keep.require.expected.js b/test/fixtures/keep.require.expected.js new file mode 100644 index 0000000..835d692 --- /dev/null +++ b/test/fixtures/keep.require.expected.js @@ -0,0 +1,7 @@ +'use strict'; + +require('./simple.css'); + +var styles = { + 'simple': '_simple_jvai8_1' +}; // @related-file ./simple.css diff --git a/test/fixtures/keep.require.in.expression.expected.js b/test/fixtures/keep.require.in.expression.expected.js new file mode 100644 index 0000000..e5fecee --- /dev/null +++ b/test/fixtures/keep.require.in.expression.expected.js @@ -0,0 +1,11 @@ +'use strict'; + +require('./simple.css'); + +var doc = { + styles: { + 'simple': '_simple_jvai8_1' + } // @related-file ./simple.css + , + title: 'test' +}; diff --git a/test/fixtures/keep.require.no.name.expected.js b/test/fixtures/keep.require.no.name.expected.js new file mode 100644 index 0000000..65842d0 --- /dev/null +++ b/test/fixtures/keep.require.no.name.expected.js @@ -0,0 +1,3 @@ +'use strict'; + +require('./simple.css'); diff --git a/test/fixtures/require.in.expression.js b/test/fixtures/require.in.expression.js new file mode 100644 index 0000000..defc90d --- /dev/null +++ b/test/fixtures/require.in.expression.js @@ -0,0 +1,4 @@ +var doc = { + styles: require('./simple.css'), + title: 'test' +} diff --git a/test/fixtures/require.no.name.js b/test/fixtures/require.no.name.js new file mode 100644 index 0000000..69f919f --- /dev/null +++ b/test/fixtures/require.no.name.js @@ -0,0 +1 @@ +require('./simple.css'); diff --git a/test/helpers.js b/test/helpers.js index daab7d5..a9ab74b 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -14,6 +14,7 @@ export const transform = ( filename: string, babelOptionOverrides: ?{ [string]: mixed }, extensions: ?string[], + advancedOptions: ?{ [string]: mixed } ): Promise => { const file = path.join(fixtures, filename); @@ -21,10 +22,10 @@ export const transform = ( babelrc: false, presets: [ ['env', { targets: { node: 'current' } }] ], plugins: [ - ['../../src/plugin.js', { + ['../../src/plugin.js', Object.assign({ config: 'fixtures/postcss.config.js', extensions, - }], + }, advancedOptions)], ], }, babelOptionOverrides); diff --git a/test/plugin.js b/test/plugin.js index a722d7e..a4e578f 100644 --- a/test/plugin.js +++ b/test/plugin.js @@ -104,6 +104,18 @@ describe('babel-plugin-transform-postcss', () => { shouldBehaveLikeSeverIsRunning(); }); + describe('when transforming require.no.name.js', () => { + beforeEach(() => transform('require.no.name.js', babelNoModules)); + + it('does not launch the server', () => { + expect(childProcess.spawn).to.not.have.been.called; + }); + + it('does not launch a client', () => { + expect(childProcess.execFileSync).to.not.have.been.called; + }); + }); + describe('when transforming import.js', () => { let result; @@ -208,4 +220,102 @@ describe('babel-plugin-transform-postcss', () => { }); }); + describe('when keepImport enabled with import.js', () => { + let result; + + beforeEach(async() => { + result = await transform( + 'import.js', + null, + ['.css'], + { + keepImport: true, + } + ); + }); + + it('compiles correctly', async() => { + expect(result).to.eql((await read('keep.import.expected.js')).trim()); + }); + }); + + describe('when keepImport enabled with require.js', () => { + let result; + + beforeEach(async() => { + result = await transform( + 'require.js', + null, + ['.css'], + { + keepImport: true, + } + ); + }); + + it('compiles correctly', async() => { + expect(result).to.eql((await read('keep.require.expected.js')).trim()); + }); + }); + + describe('when keepImport enabled with import.no.name.js', () => { + let result; + + beforeEach(async() => { + result = await transform( + 'import.no.name.js', + null, + ['.css'], + { + keepImport: true, + } + ); + }); + + it('compiles correctly', async() => { + expect(result) + .to.eql((await read('keep.import.no.name.expected.js')).trim()); + }); + }); + + describe('when keepImport enabled with require.no.name.js', () => { + let result; + + beforeEach(async() => { + result = await transform( + 'require.no.name.js', + null, + ['.css'], + { + keepImport: true, + } + ); + }); + + it('compiles correctly', async() => { + expect(result) + .to.eql((await read('keep.require.no.name.expected.js')).trim()); + }); + }); + + describe('when keepImport enabled with require.in.expression.js', () => { + let result; + + beforeEach(async() => { + result = await transform( + 'require.in.expression.js', + null, + ['.css'], + { + keepImport: true, + } + ); + }); + + it('compiles correctly', async() => { + expect(result) + .to.eql((await read('keep.require.in.expression.expected.js')).trim()); + }); + }); + });