diff --git a/src/index.js b/src/index.js index 977daeb2..c4e08cf6 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ -import { getOptions, isUrlRequest } from 'loader-utils'; +import { getOptions, isUrlRequest, stringifyRequest } from 'loader-utils'; import postcss from 'postcss'; import postcssPkg from 'postcss/package.json'; import validateOptions from 'schema-utils'; @@ -13,6 +13,7 @@ import Warning from './Warning'; import schema from './options.json'; import { icssParser, importParser, urlParser } from './plugins'; import { + getPreRequester, getExportCode, getFilter, getImportCode, @@ -38,13 +39,17 @@ export default function loader(content, map, meta) { } const exportType = options.onlyLocals ? 'locals' : 'full'; + const preRequester = getPreRequester(this); + const urlHandler = (url) => + stringifyRequest(this, preRequester(options.importLoaders) + url); - plugins.push(icssParser()); + plugins.push(icssParser({ urlHandler })); if (options.import !== false && exportType === 'full') { plugins.push( importParser({ filter: getFilter(options.import, this.resourcePath), + urlHandler, }) ); } @@ -55,6 +60,7 @@ export default function loader(content, map, meta) { filter: getFilter(options.url, this.resourcePath, (value) => isUrlRequest(value) ), + urlHandler: (url) => stringifyRequest(this, url), }) ); } @@ -118,30 +124,21 @@ export default function loader(content, map, meta) { } } - const { importLoaders, localsConvention } = options; + const { localsConvention } = options; const esModule = typeof options.esModule !== 'undefined' ? options.esModule : false; - const importCode = getImportCode( - this, - imports, - exportType, - importLoaders, - esModule - ); + const importCode = getImportCode(this, exportType, imports, esModule); const moduleCode = getModuleCode( - this, result, exportType, - esModule, sourceMap, - importLoaders, apiImports, urlReplacements, - icssReplacements + icssReplacements, + esModule ); const exportCode = getExportCode( - this, exports, exportType, localsConvention, diff --git a/src/options.json b/src/options.json index e3125073..77015844 100644 --- a/src/options.json +++ b/src/options.json @@ -83,7 +83,7 @@ "type": "boolean" }, { - "type": "number" + "type": "integer" } ] }, diff --git a/src/plugins/postcss-icss-parser.js b/src/plugins/postcss-icss-parser.js index e86d0caf..f8023c34 100644 --- a/src/plugins/postcss-icss-parser.js +++ b/src/plugins/postcss-icss-parser.js @@ -28,50 +28,56 @@ function makeRequestableIcssImports(icssImports) { }, {}); } -export default postcss.plugin('postcss-icss-parser', () => (css, result) => { - const importReplacements = Object.create(null); - const extractedICSS = extractICSS(css); - const icssImports = makeRequestableIcssImports(extractedICSS.icssImports); - - for (const [importIndex, url] of Object.keys(icssImports).entries()) { - const importName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}___`; - - result.messages.push( - { - type: 'import', - value: { type: 'icss', importName, url }, - }, - { - type: 'api-import', - value: { type: 'internal', importName, dedupe: true }, +export default postcss.plugin( + 'postcss-icss-parser', + (options) => (css, result) => { + const importReplacements = Object.create(null); + const extractedICSS = extractICSS(css); + const icssImports = makeRequestableIcssImports(extractedICSS.icssImports); + + for (const [importIndex, url] of Object.keys(icssImports).entries()) { + const importName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}___`; + + result.messages.push( + { + type: 'import', + value: { + importName, + url: options.urlHandler ? options.urlHandler(url) : url, + }, + }, + { + type: 'api-import', + value: { type: 'internal', importName, dedupe: true }, + } + ); + + const tokenMap = icssImports[url]; + const tokens = Object.keys(tokenMap); + + for (const [replacementIndex, token] of tokens.entries()) { + const replacementName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}_REPLACEMENT_${replacementIndex}___`; + const localName = tokenMap[token]; + + importReplacements[token] = replacementName; + + result.messages.push({ + type: 'icss-replacement', + value: { replacementName, importName, localName }, + }); } - ); - - const tokenMap = icssImports[url]; - const tokens = Object.keys(tokenMap); - - for (const [replacementIndex, token] of tokens.entries()) { - const replacementName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}_REPLACEMENT_${replacementIndex}___`; - const localName = tokenMap[token]; - - importReplacements[token] = replacementName; - - result.messages.push({ - type: 'icss-replacement', - value: { replacementName, importName, localName }, - }); } - } - if (Object.keys(importReplacements).length > 0) { - replaceSymbols(css, importReplacements); - } + if (Object.keys(importReplacements).length > 0) { + replaceSymbols(css, importReplacements); + } - const { icssExports } = extractedICSS; + const { icssExports } = extractedICSS; - for (const name of Object.keys(icssExports)) { - const value = replaceValueSymbols(icssExports[name], importReplacements); + for (const name of Object.keys(icssExports)) { + const value = replaceValueSymbols(icssExports[name], importReplacements); - result.messages.push({ type: 'export', value: { name, value } }); + result.messages.push({ type: 'export', value: { name, value } }); + } } -}); +); diff --git a/src/plugins/postcss-import-parser.js b/src/plugins/postcss-import-parser.js index 2399b04e..591ee3a9 100644 --- a/src/plugins/postcss-import-parser.js +++ b/src/plugins/postcss-import-parser.js @@ -108,7 +108,10 @@ export default postcss.plugin(pluginName, (options) => (css, result) => { result.messages.push({ type: 'import', - value: { type: '@import', importName, url }, + value: { + importName, + url: options.urlHandler ? options.urlHandler(url) : url, + }, }); } diff --git a/src/plugins/postcss-url-parser.js b/src/plugins/postcss-url-parser.js index e8493a84..54de6d8a 100644 --- a/src/plugins/postcss-url-parser.js +++ b/src/plugins/postcss-url-parser.js @@ -73,7 +73,8 @@ export default postcss.plugin(pluginName, (options) => (css, result) => { const parsed = valueParser(decl.value); walkUrls(parsed, (node, url, needQuotes, isStringValue) => { - if (url.trim().replace(/\\[\r\n]/g, '').length === 0) { + // https://www.w3.org/TR/css-syntax-3/#typedef-url-token + if (url.replace(/^[\s]+|[\s]+$/g, '').length === 0) { result.warn( `Unable to find uri in '${decl ? decl.toString() : decl.value}'`, { node: decl } @@ -103,13 +104,16 @@ export default postcss.plugin(pluginName, (options) => (css, result) => { importsMap.set(importKey, importName); if (!hasHelper) { + const urlToHelper = require.resolve('../runtime/getUrl.js'); + result.messages.push({ pluginName, type: 'import', value: { - type: 'url', importName: '___CSS_LOADER_GET_URL_IMPORT___', - url: require.resolve('../runtime/getUrl.js'), + url: options.urlHandler + ? options.urlHandler(urlToHelper) + : urlToHelper, }, }); @@ -120,9 +124,10 @@ export default postcss.plugin(pluginName, (options) => (css, result) => { pluginName, type: 'import', value: { - type: 'url', importName, - url: normalizedUrl, + url: options.urlHandler + ? options.urlHandler(normalizedUrl) + : normalizedUrl, }, }); } diff --git a/src/utils.js b/src/utils.js index abed024c..cd53cbeb 100644 --- a/src/utils.js +++ b/src/utils.js @@ -178,30 +178,33 @@ function normalizeSourceMap(map) { return newMap; } -function getImportPrefix(loaderContext, importLoaders) { - if (importLoaders === false) { - return ''; - } +function getPreRequester({ loaders, loaderIndex }) { + const cache = Object.create(null); - const numberImportedLoaders = parseInt(importLoaders, 10) || 0; - const loadersRequest = loaderContext.loaders - .slice( - loaderContext.loaderIndex, - loaderContext.loaderIndex + 1 + numberImportedLoaders - ) - .map((x) => x.request) - .join('!'); + return (number) => { + if (cache[number]) { + return cache[number]; + } - return `-!${loadersRequest}!`; + if (number === false) { + cache[number] = ''; + } else { + const loadersRequest = loaders + .slice( + loaderIndex, + loaderIndex + 1 + (typeof number !== 'number' ? 0 : number) + ) + .map((x) => x.request) + .join('!'); + + cache[number] = `-!${loadersRequest}!`; + } + + return cache[number]; + }; } -function getImportCode( - loaderContext, - imports, - exportType, - importLoaders, - esModule -) { +function getImportCode(loaderContext, exportType, imports, esModule) { let code = ''; if (exportType === 'full') { @@ -217,31 +220,23 @@ function getImportCode( for (const item of imports) { const { importName, url } = item; - const importUrl = stringifyRequest( - loaderContext, - item.type !== 'url' - ? getImportPrefix(loaderContext, importLoaders) + url - : url - ); code += esModule - ? `import ${importName} from ${importUrl};\n` - : `var ${importName} = require(${importUrl});\n`; + ? `import ${importName} from ${url};\n` + : `var ${importName} = require(${url});\n`; } return code ? `// Imports\n${code}` : ''; } function getModuleCode( - loaderContext, result, exportType, - esModule, sourceMap, - importLoaders, apiImports, urlReplacements, - icssReplacements + icssReplacements, + esModule ) { if (exportType !== 'full') { return ''; @@ -306,7 +301,6 @@ function dashesCamelCase(str) { } function getExportCode( - loaderContext, exports, exportType, localsConvention, @@ -393,6 +387,7 @@ export { getFilter, getModulesPlugins, normalizeSourceMap, + getPreRequester, getImportCode, getModuleCode, getExportCode, diff --git a/test/__snapshots__/importLoaders-option.test.js.snap b/test/__snapshots__/importLoaders-option.test.js.snap index ec9a1d38..ce0c790b 100644 --- a/test/__snapshots__/importLoaders-option.test.js.snap +++ b/test/__snapshots__/importLoaders-option.test.js.snap @@ -6,8 +6,10 @@ exports[`"importLoaders" option should work when not specified: module 1`] = ` "// Imports var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../src/runtime/api.js\\"); var ___CSS_LOADER_AT_RULE_IMPORT_0___ = require(\\"-!../../../src/index.js!./imported.css\\"); +var ___CSS_LOADER_AT_RULE_IMPORT_1___ = require(\\"-!../../../src/index.js!./other-imported.css\\"); exports = ___CSS_LOADER_API_IMPORT___(false); exports.i(___CSS_LOADER_AT_RULE_IMPORT_0___); +exports.i(___CSS_LOADER_AT_RULE_IMPORT_1___); // Module exports.push([module.id, \\".foo {\\\\n color: red;\\\\n color: rgba(0, 0, 255, 0.9);\\\\n}\\\\n\\", \\"\\"]); // Exports @@ -23,6 +25,15 @@ Array [ color: blue; color: rgb(0 0 100% / 90%); } +", + "", + ], + Array [ + "../../src/index.js!./nested-import/other-imported.css", + ".baz { + color: green; + color: rgb(0 0 100% / 90%); +} ", "", ], @@ -46,8 +57,10 @@ exports[`"importLoaders" option should work with a value equal to "0" (\`postcss "// Imports var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../src/runtime/api.js\\"); var ___CSS_LOADER_AT_RULE_IMPORT_0___ = require(\\"-!../../../src/index.js??[ident]!./imported.css\\"); +var ___CSS_LOADER_AT_RULE_IMPORT_1___ = require(\\"-!../../../src/index.js??[ident]!./other-imported.css\\"); exports = ___CSS_LOADER_API_IMPORT___(false); exports.i(___CSS_LOADER_AT_RULE_IMPORT_0___); +exports.i(___CSS_LOADER_AT_RULE_IMPORT_1___); // Module exports.push([module.id, \\".foo {\\\\n color: red;\\\\n color: rgba(0, 0, 255, 0.9);\\\\n}\\\\n\\", \\"\\"]); // Exports @@ -63,6 +76,15 @@ Array [ color: blue; color: rgb(0 0 100% / 90%); } +", + "", + ], + Array [ + "../../src/index.js?[ident]!./nested-import/other-imported.css", + ".baz { + color: green; + color: rgb(0 0 100% / 90%); +} ", "", ], @@ -86,8 +108,10 @@ exports[`"importLoaders" option should work with a value equal to "1" ("postcss- "// Imports var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../src/runtime/api.js\\"); var ___CSS_LOADER_AT_RULE_IMPORT_0___ = require(\\"-!../../../src/index.js??[ident]!./imported.css\\"); +var ___CSS_LOADER_AT_RULE_IMPORT_1___ = require(\\"-!../../../src/index.js??[ident]!./other-imported.css\\"); exports = ___CSS_LOADER_API_IMPORT___(false); exports.i(___CSS_LOADER_AT_RULE_IMPORT_0___); +exports.i(___CSS_LOADER_AT_RULE_IMPORT_1___); // Module exports.push([module.id, \\".foo {\\\\n color: red;\\\\n color: rgba(0, 0, 255, 0.9);\\\\n}\\\\n\\", \\"\\"]); // Exports @@ -103,6 +127,15 @@ Array [ color: blue; color: rgba(0, 0, 255, 0.9); } +", + "", + ], + Array [ + "../../src/index.js?[ident]!./nested-import/other-imported.css", + ".baz { + color: green; + color: rgba(0, 0, 255, 0.9); +} ", "", ], @@ -126,8 +159,10 @@ exports[`"importLoaders" option should work with a value equal to "1" (no loader "// Imports var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../src/runtime/api.js\\"); var ___CSS_LOADER_AT_RULE_IMPORT_0___ = require(\\"-!../../../src/index.js??[ident]!./imported.css\\"); +var ___CSS_LOADER_AT_RULE_IMPORT_1___ = require(\\"-!../../../src/index.js??[ident]!./other-imported.css\\"); exports = ___CSS_LOADER_API_IMPORT___(false); exports.i(___CSS_LOADER_AT_RULE_IMPORT_0___); +exports.i(___CSS_LOADER_AT_RULE_IMPORT_1___); // Module exports.push([module.id, \\".foo {\\\\n color: red;\\\\n color: rgb(0 0 100% / 90%);\\\\n}\\\\n\\", \\"\\"]); // Exports @@ -143,6 +178,15 @@ Array [ color: blue; color: rgb(0 0 100% / 90%); } +", + "", + ], + Array [ + "../../src/index.js?[ident]!./nested-import/other-imported.css", + ".baz { + color: green; + color: rgb(0 0 100% / 90%); +} ", "", ], @@ -166,8 +210,10 @@ exports[`"importLoaders" option should work with a value equal to "2" ("postcss- "// Imports var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../src/runtime/api.js\\"); var ___CSS_LOADER_AT_RULE_IMPORT_0___ = require(\\"-!../../../src/index.js??[ident]!./imported.css\\"); +var ___CSS_LOADER_AT_RULE_IMPORT_1___ = require(\\"-!../../../src/index.js??[ident]!./other-imported.css\\"); exports = ___CSS_LOADER_API_IMPORT___(false); exports.i(___CSS_LOADER_AT_RULE_IMPORT_0___); +exports.i(___CSS_LOADER_AT_RULE_IMPORT_1___); // Module exports.push([module.id, \\".foo {\\\\n color: red;\\\\n color: rgba(0, 0, 255, 0.9);\\\\n}\\\\n\\", \\"\\"]); // Exports @@ -183,6 +229,15 @@ Array [ color: blue; color: rgba(0, 0, 255, 0.9); } +", + "", + ], + Array [ + "../../src/index.js?[ident]!./nested-import/other-imported.css", + ".baz { + color: green; + color: rgba(0, 0, 255, 0.9); +} ", "", ], diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index 0f296761..9fefe6db 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -124,6 +124,21 @@ a:hover { exports[`loader should reuse \`ast\` from "postcss-loader": warnings 1`] = `Array []`; +exports[`loader should throw an error on invisible spaces: errors 1`] = ` +Array [ + "ModuleBuildError: Module build failed (from \`replaced original path\`): +CssSyntaxError + +(1:8) Unknown word + +> 1 | a { 

 color: red; 

 } + | ^ +", +] +`; + +exports[`loader should throw an error on invisible spaces: warnings 1`] = `Array []`; + exports[`loader should throw error on invalid css syntax: errors 1`] = ` Array [ "ModuleBuildError: Module build failed (from \`replaced original path\`): diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 4a8e10e8..f65d36cc 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -19,11 +19,21 @@ exports[`validate options should throw an error on the "import" option with "tru exports[`validate options should throw an error on the "importLoaders" option with "1" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.importLoaders should be one of these: - boolean | number + boolean | integer -> Enables/Disables or setups number of loaders applied before CSS loader (https://github.com/webpack-contrib/css-loader#importloaders). Details: * options.importLoaders should be a boolean. - * options.importLoaders should be a number." + * options.importLoaders should be a integer." +`; + +exports[`validate options should throw an error on the "importLoaders" option with "2.5" value 1`] = ` +"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. + - options.importLoaders should be one of these: + boolean | integer + -> Enables/Disables or setups number of loaders applied before CSS loader (https://github.com/webpack-contrib/css-loader#importloaders). + Details: + * options.importLoaders should be a boolean. + * options.importLoaders should be a integer." `; exports[`validate options should throw an error on the "localsConvention" option with "unknown" value 1`] = ` diff --git a/test/fixtures/invisible-space.css b/test/fixtures/invisible-space.css new file mode 100644 index 00000000..6d22c4f8 --- /dev/null +++ b/test/fixtures/invisible-space.css @@ -0,0 +1 @@ +a { 

 color: red; 

 } \ No newline at end of file diff --git a/test/fixtures/invisible-space.js b/test/fixtures/invisible-space.js new file mode 100644 index 00000000..0b273d7b --- /dev/null +++ b/test/fixtures/invisible-space.js @@ -0,0 +1,5 @@ +import css from './invisible-space.css'; + +__export__ = css; + +export default css; diff --git a/test/fixtures/nested-import/other-imported.css b/test/fixtures/nested-import/other-imported.css new file mode 100644 index 00000000..051e9d94 --- /dev/null +++ b/test/fixtures/nested-import/other-imported.css @@ -0,0 +1,4 @@ +.baz { + color: green; + color: rgb(0 0 100% / 90%); +} diff --git a/test/fixtures/nested-import/source.css b/test/fixtures/nested-import/source.css index 25fa4ca1..6aeedd42 100644 --- a/test/fixtures/nested-import/source.css +++ b/test/fixtures/nested-import/source.css @@ -1,4 +1,5 @@ @import './imported.css'; +@import './other-imported.css'; .foo { color: red; diff --git a/test/loader.test.js b/test/loader.test.js index 1c16d48d..af438628 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -11,6 +11,8 @@ import { getWarnings, } from './helpers/index'; +jest.setTimeout(10000); + describe('loader', () => { it('should work', async () => { const compiler = getCompiler('./basic.js'); @@ -342,4 +344,12 @@ describe('loader', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); expect(getErrors(stats)).toMatchSnapshot('errors'); }); + + it('should throw an error on invisible spaces', async () => { + const compiler = getCompiler('./invisible-space.js'); + const stats = await compile(compiler); + + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); }); diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 814ee799..969765e4 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -53,7 +53,7 @@ describe('validate options', () => { }, importLoaders: { success: [false, 0, 1, 2], - failure: ['1'], + failure: ['1', 2.5], }, onlyLocals: { success: [true, false],