From aa148382105e324ca218e2f2efeae92f75eafb01 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Mon, 10 Feb 2020 11:03:41 -0500 Subject: [PATCH 1/4] feat: re-use postcss ast if passed from previous loader --- package.json | 3 +- src/index.js | 22 +++++++++++++-- test/__snapshots__/loader.test.js.snap | 2 ++ test/ast-loader.js | 28 +++++++++++++++++++ test/loader.test.js | 29 +++++++++++++++++++- test/options/__snapshots__/exec.test.js.snap | 5 +--- test/options/exec.test.js | 1 + 7 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 test/ast-loader.js diff --git a/package.json b/package.json index 0a317336..285a9e3b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "loader-utils": "^1.1.0", "postcss": "^7.0.0", "postcss-load-config": "^2.0.0", - "schema-utils": "^1.0.0" + "schema-utils": "^1.0.0", + "semver": "^6.3.0" }, "devDependencies": { "@webpack-utilities/test": "^1.0.0-alpha.0", diff --git a/src/index.js b/src/index.js index bb576c32..6f87882e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,12 @@ const path = require('path') +const { satisfies } = require('semver') + const { getOptions } = require('loader-utils') const validateOptions = require('schema-utils') const postcss = require('postcss') +const postcssPkg = require('postcss/package.json') const postcssrc = require('postcss-load-config') const Warning = require('./Warning.js') @@ -32,6 +35,16 @@ function loader (css, map, meta) { const sourceMap = options.sourceMap + let prevAst = null + if ( + meta && + meta.ast && + meta.ast.type === 'postcss' && + satisfies(meta.ast.version, `^${postcssPkg.version}`) + ) { + prevAst = meta.ast.root + } + Promise.resolve().then(() => { const length = Object.keys(options) .filter((option) => { @@ -107,7 +120,7 @@ function loader (css, map, meta) { // Loader Exec (Deprecated) // https://webpack.js.org/api/loaders/#deprecated-context-properties - if (options.parser === 'postcss-js') { + if (!prevAst && options.parser === 'postcss-js') { css = this.exec(css, this.resource) } @@ -125,7 +138,7 @@ function loader (css, map, meta) { // Loader API Exec (Deprecated) // https://webpack.js.org/api/loaders/#deprecated-context-properties - if (config.exec) { + if (!prevAst && config.exec) { css = this.exec(css, this.resource) } @@ -136,9 +149,10 @@ function loader (css, map, meta) { if (sourceMap && map) { options.map.prev = map } + const content = prevAst || css return postcss(plugins) - .process(css, options) + .process(content, options) .then((result) => { let { css, map, root, processor, messages } = result @@ -219,6 +233,8 @@ function loader (css, map, meta) { * * @requires path * + * @requires semver + * * @requires loader-utils * @requires schema-utils * diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index 97a69ae8..37d7a2ea 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -1,3 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Loader Default 1`] = `"module.exports = \\"a { color: black }\\\\n\\""`; + +exports[`Loader uses previous AST 1`] = `"module.exports = \\"a {\\\\n color: yellow\\\\n}\\""`; diff --git a/test/ast-loader.js b/test/ast-loader.js new file mode 100644 index 00000000..925d9fcd --- /dev/null +++ b/test/ast-loader.js @@ -0,0 +1,28 @@ +const postcss = require('postcss') +const postcssPkg = require('postcss/package.json') +const semver = require('semver') + +const incomingVersion = semver.inc(postcssPkg.version, 'minor') + +module.exports = function astLoader (content) { + const callback = this.async() + + const { spy = jest.fn() } = this.query + + content = this.exec(content, this.resource) + + postcss() + .process(content, { parser: require('postcss-js') }) + .then(({ css, map, root, messages }) => { + const ast = { + type: 'postcss', + version: incomingVersion + } + + Object.defineProperty(ast, 'root', { + get: spy.mockReturnValue(root) + }) + + callback(null, css, map, { ast, messages }) + }) +} diff --git a/test/loader.test.js b/test/loader.test.js index 8c8ae36d..96b57881 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -1,5 +1,4 @@ const { webpack } = require('@webpack-utilities/test') - describe('Loader', () => { test('Default', () => { const config = { @@ -18,4 +17,32 @@ describe('Loader', () => { expect(source).toMatchSnapshot() }) }) + + test.only('uses previous AST', () => { + const spy = jest.fn() + const config = { + rules: [ + { + test: /style\.js$/, + use: [ + { + loader: require.resolve('../src'), + options: { importLoaders: 1 } + }, + { + loader: require.resolve('./ast-loader'), + options: { spy } + } + ] + } + ] + } + + return webpack('jss/index.js', config).then((stats) => { + const { source } = stats.toJson().modules[1] + + expect(spy).toHaveBeenCalledTimes(1) + expect(source).toMatchSnapshot() + }) + }) }) diff --git a/test/options/__snapshots__/exec.test.js.snap b/test/options/__snapshots__/exec.test.js.snap index e9c24e8c..372c9e37 100644 --- a/test/options/__snapshots__/exec.test.js.snap +++ b/test/options/__snapshots__/exec.test.js.snap @@ -2,7 +2,4 @@ exports[`Options Exec - {Boolean} 1`] = `"module.exports = \\"a {\\\\n color: green\\\\n}\\""`; -exports[`Options JSS - {String} 1`] = ` -"module.exports = { a: { color: 'yellow' } } -" -`; +exports[`Options JSS - {String} 1`] = `"module.exports = \\"a {\\\\n color: yellow\\\\n}\\""`; diff --git a/test/options/exec.test.js b/test/options/exec.test.js index 45716078..ab601489 100644 --- a/test/options/exec.test.js +++ b/test/options/exec.test.js @@ -25,6 +25,7 @@ describe('Options', () => { test('JSS - {String}', () => { const config = { loader: { + test: /style\.js$/, options: { parser: 'postcss-js' } From 8c62d96be9f19b7e57d364584ea7395787167d86 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Mon, 10 Feb 2020 12:09:49 -0500 Subject: [PATCH 2/4] test: update snapshots --- test/options/__snapshots__/sourceMap.test.js.snap | 4 ++-- test/options/sourceMap.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/options/__snapshots__/sourceMap.test.js.snap b/test/options/__snapshots__/sourceMap.test.js.snap index 63c09174..46f1e9e2 100644 --- a/test/options/__snapshots__/sourceMap.test.js.snap +++ b/test/options/__snapshots__/sourceMap.test.js.snap @@ -5,7 +5,7 @@ exports[`Options Sourcemap - {Boolean} 1`] = `"module.exports = \\"a { color: rg exports[`Options Sourcemap - {Boolean} 2`] = ` Object { "file": "../fixtures/css/style.css", - "mappings": "AAAA,IAAI,2BAAY,EAAE", + "mappings": "AAAA,IAAI,4BAAa", "names": Array [], "sources": Array [ "../fixtures/css/style.css", @@ -18,4 +18,4 @@ Object { } `; -exports[`Options Sourcemap - {String} 1`] = `"module.exports = \\"a { color: rgba(255, 0, 0, 1.0) }\\\\n\\\\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvZml4dHVyZXMvY3NzL3N0eWxlLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxJQUFJLDJCQUFZLEVBQUUiLCJmaWxlIjoidGVzdC9maXh0dXJlcy9jc3Mvc3R5bGUuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiYSB7IGNvbG9yOiBibGFjayB9XG4iXX0= */\\""`; +exports[`Options Sourcemap - {String} 1`] = `"module.exports = \\"a { color: rgba(255, 0, 0, 1.0) }\\\\n\\\\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvZml4dHVyZXMvY3NzL3N0eWxlLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxJQUFJLDRCQUFhIiwiZmlsZSI6InRlc3QvZml4dHVyZXMvY3NzL3N0eWxlLmNzcyIsInNvdXJjZXNDb250ZW50IjpbImEgeyBjb2xvcjogYmxhY2sgfVxuIl19 */\\""`; diff --git a/test/options/sourceMap.test.js b/test/options/sourceMap.test.js index b321c77f..26d6817d 100644 --- a/test/options/sourceMap.test.js +++ b/test/options/sourceMap.test.js @@ -47,7 +47,7 @@ describe('Options', () => { const { source } = stats.toJson().modules[1] expect(source).toEqual( - 'module.exports = "a { color: rgba(255, 0, 0, 1.0) }\\n\\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvZml4dHVyZXMvY3NzL3N0eWxlLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxJQUFJLDJCQUFZLEVBQUUiLCJmaWxlIjoidGVzdC9maXh0dXJlcy9jc3Mvc3R5bGUuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiYSB7IGNvbG9yOiBibGFjayB9XG4iXX0= */"' + 'module.exports = "a { color: rgba(255, 0, 0, 1.0) }\\n\\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvZml4dHVyZXMvY3NzL3N0eWxlLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxJQUFJLDRCQUFhIiwiZmlsZSI6InRlc3QvZml4dHVyZXMvY3NzL3N0eWxlLmNzcyIsInNvdXJjZXNDb250ZW50IjpbImEgeyBjb2xvcjogYmxhY2sgfVxuIl19 */"' ) expect(source).toMatchSnapshot() From 12b3a8106e158989b9c960aa48d5c7f18cc8b19f Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Wed, 13 May 2020 09:13:09 -0400 Subject: [PATCH 3/4] WIP --- README.md | 70 +++ src/index.js | 424 +++++++++--------- src/index.old.js | 248 ++++++++++ src/options.js | 36 -- test/__snapshots__/custom-loader.test.js.snap | 5 + test/ast-loader.js | 1 + test/custom-loader.js | 22 + test/custom-loader.test.js | 48 ++ test/loader.test.js | 2 +- 9 files changed, 608 insertions(+), 248 deletions(-) create mode 100644 src/index.old.js delete mode 100644 src/options.js create mode 100644 test/__snapshots__/custom-loader.test.js.snap create mode 100644 test/custom-loader.js create mode 100644 test/custom-loader.test.js diff --git a/README.md b/README.md index e5ddad4f..b5e68c87 100644 --- a/README.md +++ b/README.md @@ -454,6 +454,76 @@ module.exports = { } ``` +## Customized Loader + +In order to make it easier to customize new tooling on top of `postcss-loader`, it exposes a +loader-builder utility to allow adding custom config and logic handling for each file +the laoder processes. + +`postcssLoader.custom` accepts a callback that will be called with the instance of `postcss` +used by the loader, so tooling can ensure that it's using the same `postcss` as the loader itself. + +```js +const postcssLoader = require('postcss-loader'); + +const utilityPostcssLoader = postcssLoader.custom((postcss) => { + return { + // customOptions is passed any user configured loader options + // You can use this hook to split out or your own options from the base loader's options + customOptions: ({ autoprefix, ...loader }) => ({ + custom: { autoprefix } + loader, + }) + // config provides a hook for customizing parsed postcss config file (or options from loader) + config(config, options) { + // file will be a path if parsed from a postcss.config.js file + const { file, plugins, options } = config + // the input css string, ast and input (if they exist) + // as well as the options parsed out in customOptions above + const { source, inputAst, inputMap, customOptions } = options + + // Add custom handling + + if (!customOptions.autoprefix) { + return config; + } + + return { + ...config, + plugins: [ + ...config.plugins, + require('autoprefixer'), + ], + } + } + + // a hook for postprocessing the result of the loader. + // passed the postcss Result + result(result, options) { + return { + ...result, + css: result.css + "\n// Generated by some custom loader", + }; + }, + } +}) +``` + +```js +// And in your Webpack config +module.exports = { + // .. + module: { + rules: [{ + // ... + loader: path.join(__dirname, './utility-loader.js'), + options: { autoprefix: true } + // ... + }] + } +}; +``` +

Maintainers

diff --git a/src/index.js b/src/index.js index 6f87882e..a004fca2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,248 +1,250 @@ -const path = require('path') +const path = require('path'); -const { satisfies } = require('semver') - -const { getOptions } = require('loader-utils') +const { getOptions } = require('loader-utils'); const validateOptions = require('schema-utils') - -const postcss = require('postcss') -const postcssPkg = require('postcss/package.json') -const postcssrc = require('postcss-load-config') - +const postcss = require('postcss'); +const postcssrc = require('postcss-load-config'); +const postcssPkg = require('postcss/package.json'); +const { satisfies } = require('semver'); const Warning = require('./Warning.js') const SyntaxError = require('./Error.js') -const parseOptions = require('./options.js') - -/** - * **PostCSS Loader** - * - * Loads && processes CSS with [PostCSS](https://github.com/postcss/postcss) - * - * @method loader - * - * @param {String} css Source - * @param {Object} map Source Map - * - * @return {cb} cb Result - */ -function loader (css, map, meta) { - const options = Object.assign({}, getOptions(this)) - - validateOptions(require('./options.json'), options, 'PostCSS Loader') - - const cb = this.async() - const file = this.resourcePath - - const sourceMap = options.sourceMap - - let prevAst = null + + +function createLoader(fn) { + const overrides = fn ? fn(postcss) : undefined; + + return function customPostcssLoader(source, inputMap, meta) { + // Make the loader async + const callback = this.async(); + + // eslint-disable-next-line no-use-before-define + postcssLoader.call(this, source, inputMap, meta, overrides).then( + (args) => callback(null, ...args), + (err) => { + if (err.file) this.addDependency(err.file); + + return err.name === 'CssSyntaxError' + ? callback(new SyntaxError(err)) + : callback(err); + }, + ); + }; +} + +function getProgrammaticOptions({ parser, syntax, stringifier, plugins }) { + if (typeof plugins === 'function') { + plugins = plugins(this); + } + if (plugins == null) { + plugins = []; + } else if (!Array.isArray(plugins)) { + plugins = [plugins]; + } + + if (!parser && !syntax && !stringifier) { + return { plugins }; + } + + return { + file: null, + plugins, + options: { + parser, + syntax, + stringifier, + }, + }; +} + +async function postcssLoader(source, inputMap, meta, overrides = {}) { + const file = this.resourcePath; + + let inputAst = null; if ( meta && meta.ast && meta.ast.type === 'postcss' && satisfies(meta.ast.version, `^${postcssPkg.version}`) ) { - prevAst = meta.ast.root + inputAst = meta.ast.root; + } + + let loaderOptions = getOptions(this) || {}; + + validateOptions(require('./options.json'), loaderOptions, 'PostCSS Loader') + + + let customOptions; + if (overrides && overrides.customOptions) { + const result = await overrides.customOptions.call(this, loaderOptions, { + source, + inputAst, + inputMap, + }); + + customOptions = result.custom; + loaderOptions = result.loader; } - Promise.resolve().then(() => { - const length = Object.keys(options) - .filter((option) => { - switch (option) { - // case 'exec': - case 'ident': - case 'config': - case 'sourceMap': - return - default: - return option - } - }) - .length - - if (length) { - return parseOptions.call(this, options) + const { sourceMap } = loaderOptions; + + const getConfig = async () => { + const programmaticOptions = getProgrammaticOptions(loaderOptions); + const hasOptions = + loaderOptions.exec || + loaderOptions.parser || + loaderOptions.syntax || + loaderOptions.stringifier || + loaderOptions.plugins; + + if (hasOptions || loaderOptions.config === false) { + return programmaticOptions; } - const rc = { - path: path.dirname(file), - ctx: { - file: { - extname: path.extname(file), - dirname: path.dirname(file), - basename: path.basename(file) - }, - options: {} + let filePath = path.dirname(file); + const ctx = { + file: { + extname: path.extname(file), + dirname: path.dirname(file), + basename: path.basename(file), + }, + options: {}, + }; + + if (loaderOptions.config) { + if (loaderOptions.config.path) { + filePath = path.resolve(loaderOptions.config.path); + } + if (loaderOptions.config.ctx) { + ctx.options = loaderOptions.config.ctx; } } - if (options.config) { - if (options.config.path) { - rc.path = path.resolve(options.config.path) - } + ctx.webpack = this; - if (options.config.ctx) { - rc.ctx.options = options.config.ctx + let config = { file: null, options: {}, plugins: [] }; + try { + config = await postcssrc(ctx, filePath); + } catch (err) { + if (!err.message.includes('No PostCSS Config')) { + throw err; } } - rc.ctx.webpack = this - - return postcssrc(rc.ctx, rc.path) - }).then((config) => { - if (!config) { - config = {} + if (typeof loaderOptions.config === 'function') { + config = await loaderOptions.config.call(this, config, { + source, + inputMap, + inputAst, + }); } if (config.file) { - this.addDependency(config.file) + this.addDependency(config.file); } + return config; + }; + + let config = await getConfig(); + + if (overrides.config) { + config = await overrides.config.call(this, config, { + source, + inputMap, + inputAst, + customOptions, + }); + } - // Disable override `to` option from `postcss.config.js` - if (config.options.to) { - delete config.options.to - } - // Disable override `from` option from `postcss.config.js` - if (config.options.from) { - delete config.options.from - } + const options = { + // eslint-disable-next-line no-nested-ternary + map: sourceMap + ? sourceMap === 'inline' + ? { inline: true, annotation: false } + : { inline: false, annotation: false } + : false, - let plugins = config.plugins || [] - - let options = Object.assign({ - from: file, - map: sourceMap - ? sourceMap === 'inline' - ? { inline: true, annotation: false } - : { inline: false, annotation: false } - : false - }, config.options) - - // Loader Exec (Deprecated) - // https://webpack.js.org/api/loaders/#deprecated-context-properties - if (!prevAst && options.parser === 'postcss-js') { - css = this.exec(css, this.resource) - } + ...config.options, + from: file, + }; - if (typeof options.parser === 'string') { - options.parser = require(options.parser) - } + delete options.to; - if (typeof options.syntax === 'string') { - options.syntax = require(options.syntax) - } + // Loader Exec (Deprecated) + // https://webpack.js.org/api/loaders/#deprecated-context-properties + if (!inputAst && (options.parser === 'postcss-js' || loaderOptions.exec)) { + source = this.exec(source, this.resource) + } - if (typeof options.stringifier === 'string') { - options.stringifier = require(options.stringifier) - } + if (typeof options.parser === 'string') { + options.parser = require(options.parser); + } - // Loader API Exec (Deprecated) - // https://webpack.js.org/api/loaders/#deprecated-context-properties - if (!prevAst && config.exec) { - css = this.exec(css, this.resource) - } + if (typeof options.syntax === 'string') { + options.syntax = require(options.syntax); + } - if (sourceMap && typeof map === 'string') { - map = JSON.parse(map) - } + if (typeof options.stringifier === 'string') { + options.stringifier = require(options.stringifier); + } - if (sourceMap && map) { - options.map.prev = map - } - const content = prevAst || css - - return postcss(plugins) - .process(content, options) - .then((result) => { - let { css, map, root, processor, messages } = result - - result.warnings().forEach((warning) => { - this.emitWarning(new Warning(warning)) - }) - - messages.forEach((msg) => { - if (msg.type === 'dependency') { - this.addDependency(msg.file) - } - }) - - map = map ? map.toJSON() : null - - if (map) { - map.file = path.resolve(map.file) - map.sources = map.sources.map((src) => path.resolve(src)) - } - - if (!meta) { - meta = {} - } - - const ast = { - type: 'postcss', - version: processor.version, - root - } - - meta.ast = ast - meta.messages = messages - - if (this.loaderIndex === 0) { - /** - * @memberof loader - * @callback cb - * - * @param {Object} null Error - * @param {String} css Result (JS Module) - * @param {Object} map Source Map - */ - cb(null, `module.exports = ${JSON.stringify(css)}`, map) - - return null - } - - /** - * @memberof loader - * @callback cb - * - * @param {Object} null Error - * @param {String} css Result (Raw Module) - * @param {Object} map Source Map - */ - cb(null, css, map, meta) - - return null - }) - }).catch((err) => { - if (err.file) { - this.addDependency(err.file) + if (sourceMap && typeof inputMap === 'string') { + inputMap = JSON.parse(inputMap); + } + + if (sourceMap && inputMap && options.map) { + options.map.prev = inputMap; + } + + const content = inputAst || source; + + let result = await postcss(config.plugins).process(content, options); + + if (overrides.result) { + result = await overrides.result.call(this, result, { + source, + inputMap, + inputAst, + customOptions, + }); + } + + let { css, map, root, processor, messages } = result; + + result.warnings().forEach((warning) => { + this.emitWarning(new Warning(warning)); + }); + + messages.forEach((msg) => { + if (msg.type === 'dependency') { + this.addDependency(msg.file); } + }); + + map = map ? map.toJSON() : null; + + if (map) { + map.file = path.resolve(map.file); + map.sources = map.sources.map((src) => path.resolve(src)); + } + + if (!meta) meta = {}; + + const ast = { + type: 'postcss', + version: processor.version, + root, + }; + + meta.ast = ast; + meta.messages = messages; + + if (this.loaderIndex === 0) { + css = `module.exports = ${JSON.stringify(css)}`; + } - return err.name === 'CssSyntaxError' - ? cb(new SyntaxError(err)) - : cb(err) - }) + return [css, map, meta]; } -/** - * @author Andrey Sitnik (@ai) - * - * @license MIT - * @version 3.0.0 - * - * @module postcss-loader - * - * @requires path - * - * @requires semver - * - * @requires loader-utils - * @requires schema-utils - * - * @requires postcss - * @requires postcss-load-config - * - * @requires ./options.js - * @requires ./Warning.js - * @requires ./SyntaxError.js - */ -module.exports = loader +module.exports = createLoader(); +module.exports.custom = createLoader; diff --git a/src/index.old.js b/src/index.old.js new file mode 100644 index 00000000..6f87882e --- /dev/null +++ b/src/index.old.js @@ -0,0 +1,248 @@ +const path = require('path') + +const { satisfies } = require('semver') + +const { getOptions } = require('loader-utils') +const validateOptions = require('schema-utils') + +const postcss = require('postcss') +const postcssPkg = require('postcss/package.json') +const postcssrc = require('postcss-load-config') + +const Warning = require('./Warning.js') +const SyntaxError = require('./Error.js') +const parseOptions = require('./options.js') + +/** + * **PostCSS Loader** + * + * Loads && processes CSS with [PostCSS](https://github.com/postcss/postcss) + * + * @method loader + * + * @param {String} css Source + * @param {Object} map Source Map + * + * @return {cb} cb Result + */ +function loader (css, map, meta) { + const options = Object.assign({}, getOptions(this)) + + validateOptions(require('./options.json'), options, 'PostCSS Loader') + + const cb = this.async() + const file = this.resourcePath + + const sourceMap = options.sourceMap + + let prevAst = null + if ( + meta && + meta.ast && + meta.ast.type === 'postcss' && + satisfies(meta.ast.version, `^${postcssPkg.version}`) + ) { + prevAst = meta.ast.root + } + + Promise.resolve().then(() => { + const length = Object.keys(options) + .filter((option) => { + switch (option) { + // case 'exec': + case 'ident': + case 'config': + case 'sourceMap': + return + default: + return option + } + }) + .length + + if (length) { + return parseOptions.call(this, options) + } + + const rc = { + path: path.dirname(file), + ctx: { + file: { + extname: path.extname(file), + dirname: path.dirname(file), + basename: path.basename(file) + }, + options: {} + } + } + + if (options.config) { + if (options.config.path) { + rc.path = path.resolve(options.config.path) + } + + if (options.config.ctx) { + rc.ctx.options = options.config.ctx + } + } + + rc.ctx.webpack = this + + return postcssrc(rc.ctx, rc.path) + }).then((config) => { + if (!config) { + config = {} + } + + if (config.file) { + this.addDependency(config.file) + } + + // Disable override `to` option from `postcss.config.js` + if (config.options.to) { + delete config.options.to + } + // Disable override `from` option from `postcss.config.js` + if (config.options.from) { + delete config.options.from + } + + let plugins = config.plugins || [] + + let options = Object.assign({ + from: file, + map: sourceMap + ? sourceMap === 'inline' + ? { inline: true, annotation: false } + : { inline: false, annotation: false } + : false + }, config.options) + + // Loader Exec (Deprecated) + // https://webpack.js.org/api/loaders/#deprecated-context-properties + if (!prevAst && options.parser === 'postcss-js') { + css = this.exec(css, this.resource) + } + + if (typeof options.parser === 'string') { + options.parser = require(options.parser) + } + + if (typeof options.syntax === 'string') { + options.syntax = require(options.syntax) + } + + if (typeof options.stringifier === 'string') { + options.stringifier = require(options.stringifier) + } + + // Loader API Exec (Deprecated) + // https://webpack.js.org/api/loaders/#deprecated-context-properties + if (!prevAst && config.exec) { + css = this.exec(css, this.resource) + } + + if (sourceMap && typeof map === 'string') { + map = JSON.parse(map) + } + + if (sourceMap && map) { + options.map.prev = map + } + const content = prevAst || css + + return postcss(plugins) + .process(content, options) + .then((result) => { + let { css, map, root, processor, messages } = result + + result.warnings().forEach((warning) => { + this.emitWarning(new Warning(warning)) + }) + + messages.forEach((msg) => { + if (msg.type === 'dependency') { + this.addDependency(msg.file) + } + }) + + map = map ? map.toJSON() : null + + if (map) { + map.file = path.resolve(map.file) + map.sources = map.sources.map((src) => path.resolve(src)) + } + + if (!meta) { + meta = {} + } + + const ast = { + type: 'postcss', + version: processor.version, + root + } + + meta.ast = ast + meta.messages = messages + + if (this.loaderIndex === 0) { + /** + * @memberof loader + * @callback cb + * + * @param {Object} null Error + * @param {String} css Result (JS Module) + * @param {Object} map Source Map + */ + cb(null, `module.exports = ${JSON.stringify(css)}`, map) + + return null + } + + /** + * @memberof loader + * @callback cb + * + * @param {Object} null Error + * @param {String} css Result (Raw Module) + * @param {Object} map Source Map + */ + cb(null, css, map, meta) + + return null + }) + }).catch((err) => { + if (err.file) { + this.addDependency(err.file) + } + + return err.name === 'CssSyntaxError' + ? cb(new SyntaxError(err)) + : cb(err) + }) +} + +/** + * @author Andrey Sitnik (@ai) + * + * @license MIT + * @version 3.0.0 + * + * @module postcss-loader + * + * @requires path + * + * @requires semver + * + * @requires loader-utils + * @requires schema-utils + * + * @requires postcss + * @requires postcss-load-config + * + * @requires ./options.js + * @requires ./Warning.js + * @requires ./SyntaxError.js + */ +module.exports = loader diff --git a/src/options.js b/src/options.js deleted file mode 100644 index fbe71369..00000000 --- a/src/options.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * **PostCSS Options Parser** - * - * Transforms the loader options into a valid postcss config `{Object}` - * - * @method parseOptions - * - * @param {Boolean} exec Execute CSS-in-JS - * @param {String|Object} parser PostCSS Parser - * @param {String|Object} syntax PostCSS Syntax - * @param {String|Object} stringifier PostCSS Stringifier - * @param {Array|Object|Function} plugins PostCSS Plugins - * - * @return {Promise} PostCSS Config - */ -function parseOptions ({ exec, parser, syntax, stringifier, plugins }) { - if (typeof plugins === 'function') { - plugins = plugins.call(this, this) - } - - if (typeof plugins === 'undefined') { - plugins = [] - } else if (!Array.isArray(plugins)) { - plugins = [ plugins ] - } - - const options = {} - - options.parser = parser - options.syntax = syntax - options.stringifier = stringifier - - return Promise.resolve({ options, plugins, exec }) -} - -module.exports = parseOptions diff --git a/test/__snapshots__/custom-loader.test.js.snap b/test/__snapshots__/custom-loader.test.js.snap new file mode 100644 index 00000000..37d7a2ea --- /dev/null +++ b/test/__snapshots__/custom-loader.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Loader Default 1`] = `"module.exports = \\"a { color: black }\\\\n\\""`; + +exports[`Loader uses previous AST 1`] = `"module.exports = \\"a {\\\\n color: yellow\\\\n}\\""`; diff --git a/test/ast-loader.js b/test/ast-loader.js index 925d9fcd..2a99efb7 100644 --- a/test/ast-loader.js +++ b/test/ast-loader.js @@ -1,6 +1,7 @@ const postcss = require('postcss') const postcssPkg = require('postcss/package.json') const semver = require('semver') +const loader = require('../src') const incomingVersion = semver.inc(postcssPkg.version, 'minor') diff --git a/test/custom-loader.js b/test/custom-loader.js new file mode 100644 index 00000000..489d93a0 --- /dev/null +++ b/test/custom-loader.js @@ -0,0 +1,22 @@ + +const loader = require('../src') + +const incomingVersion = semver.inc(postcssPkg.version, 'minor') + +module.exports = loader.custom(postcss => ({ + customOptions: ({ resultSpy, configSpy, ...loader }) => ({ + custom: { spy }, + loader + }), + + config: (config, options) => { + options.customOptions.configSpy(config options) + return config + }, + + resul: ((result,options) => { + + options.customOptions.resultSpy(config options) + return result + }) +})) diff --git a/test/custom-loader.test.js b/test/custom-loader.test.js new file mode 100644 index 00000000..f1f44e45 --- /dev/null +++ b/test/custom-loader.test.js @@ -0,0 +1,48 @@ +const { webpack } = require('@webpack-utilities/test') +describe('Loader', () => { + test('Default', () => { + const config = { + loader: { + test: /\.css$/, + options: { + plugins: [] + } + } + } + + return webpack('css/index.js', config).then((stats) => { + const { source } = stats.toJson().modules[1] + + expect(source).toEqual('module.exports = "a { color: black }\\n"') + expect(source).toMatchSnapshot() + }) + }) + + test('uses previous AST', () => { + const spy = jest.fn() + const config = { + rules: [ + { + test: /style\.js$/, + use: [ + { + loader: require.resolve('../src'), + options: { importLoaders: 1 } + }, + { + loader: require.resolve('./ast-loader'), + options: { spy } + } + ] + } + ] + } + + return webpack('jss/index.js', config).then((stats) => { + const { source } = stats.toJson().modules[1] + + expect(spy).toHaveBeenCalledTimes(1) + expect(source).toMatchSnapshot() + }) + }) +}) diff --git a/test/loader.test.js b/test/loader.test.js index 96b57881..f1f44e45 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -18,7 +18,7 @@ describe('Loader', () => { }) }) - test.only('uses previous AST', () => { + test('uses previous AST', () => { const spy = jest.fn() const config = { rules: [ From c770fe49ebee53f23de5a833bdf2df64ce941ff8 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Wed, 13 May 2020 09:31:03 -0400 Subject: [PATCH 4/4] feat: add custom loader builder --- src/index.js | 8 --- test/__snapshots__/custom-loader.test.js.snap | 2 + test/custom-loader.js | 11 ++-- test/custom-loader.test.js | 51 +++++++------------ 4 files changed, 25 insertions(+), 47 deletions(-) diff --git a/src/index.js b/src/index.js index a004fca2..c1071672 100644 --- a/src/index.js +++ b/src/index.js @@ -131,14 +131,6 @@ async function postcssLoader(source, inputMap, meta, overrides = {}) { } } - if (typeof loaderOptions.config === 'function') { - config = await loaderOptions.config.call(this, config, { - source, - inputMap, - inputAst, - }); - } - if (config.file) { this.addDependency(config.file); } diff --git a/test/__snapshots__/custom-loader.test.js.snap b/test/__snapshots__/custom-loader.test.js.snap index 37d7a2ea..b8473daf 100644 --- a/test/__snapshots__/custom-loader.test.js.snap +++ b/test/__snapshots__/custom-loader.test.js.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Custom Loader Default 1`] = `"module.exports = \\"a { color: black }\\\\n\\""`; + exports[`Loader Default 1`] = `"module.exports = \\"a { color: black }\\\\n\\""`; exports[`Loader uses previous AST 1`] = `"module.exports = \\"a {\\\\n color: yellow\\\\n}\\""`; diff --git a/test/custom-loader.js b/test/custom-loader.js index 489d93a0..5ebbf015 100644 --- a/test/custom-loader.js +++ b/test/custom-loader.js @@ -1,22 +1,19 @@ - const loader = require('../src') -const incomingVersion = semver.inc(postcssPkg.version, 'minor') - module.exports = loader.custom(postcss => ({ customOptions: ({ resultSpy, configSpy, ...loader }) => ({ - custom: { spy }, + custom: { resultSpy, configSpy }, loader }), config: (config, options) => { - options.customOptions.configSpy(config options) + options.customOptions.configSpy(config, options) return config }, - resul: ((result,options) => { + result: ((result, options) => { + options.customOptions.resultSpy(result, options) - options.customOptions.resultSpy(config options) return result }) })) diff --git a/test/custom-loader.test.js b/test/custom-loader.test.js index f1f44e45..220147d0 100644 --- a/test/custom-loader.test.js +++ b/test/custom-loader.test.js @@ -1,48 +1,35 @@ const { webpack } = require('@webpack-utilities/test') -describe('Loader', () => { - test('Default', () => { - const config = { - loader: { - test: /\.css$/, - options: { - plugins: [] - } - } - } - return webpack('css/index.js', config).then((stats) => { - const { source } = stats.toJson().modules[1] +describe('Custom Loader', () => { + test('Default', () => { + const configSpy = jest.fn((config, options) => { + expect(config.plugins).toHaveLength(1) + expect(options.source).toContain('color: black') + }); - expect(source).toEqual('module.exports = "a { color: black }\\n"') - expect(source).toMatchSnapshot() - }) - }) + const resultSpy = jest.fn((result) => { + expect(result.css).toContain('color: rgba(255, 0, 0, 1.0)') + }); - test('uses previous AST', () => { - const spy = jest.fn() const config = { rules: [ { - test: /style\.js$/, - use: [ - { - loader: require.resolve('../src'), - options: { importLoaders: 1 } - }, - { - loader: require.resolve('./ast-loader'), - options: { spy } - } - ] + test: /\.css$/, + use: { + loader: require.resolve('./custom-loader'), + options: { configSpy, resultSpy } + } } ] } - return webpack('jss/index.js', config).then((stats) => { + return webpack('css/index.js', config).then((stats) => { const { source } = stats.toJson().modules[1] - expect(spy).toHaveBeenCalledTimes(1) - expect(source).toMatchSnapshot() + expect(source).toEqual('module.exports = "a { color: rgba(255, 0, 0, 1.0) }\\n"') + + expect(configSpy).toHaveBeenCalledTimes(1) + expect(resultSpy).toHaveBeenCalledTimes(1) }) }) })