From 8bca5262b5b3223108d14f8e10617ae6ca8dd85f Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Fri, 2 Oct 2020 15:47:20 +0300 Subject: [PATCH] feat: webpack resolver supports the `import` option --- src/utils.js | 140 ++++++++++++++++--------- test/__snapshots__/loader.test.js.snap | 36 +++++++ test/loader.test.js | 60 +++++++++++ 3 files changed, 189 insertions(+), 47 deletions(-) diff --git a/src/utils.js b/src/utils.js index 7f66634..1f97901 100644 --- a/src/utils.js +++ b/src/utils.js @@ -347,6 +347,7 @@ async function createEvaluator(loaderContext, code, options) { resolveToContext: true, }); + const resolvedImportDependencies = new Map(); const resolvedDependencies = new Map(); const seen = new Set(); @@ -361,6 +362,47 @@ async function createEvaluator(loaderContext, code, options) { options ); + const optionsImports = []; + + for (const importPath of options.imports) { + const isGlob = fastGlob.isDynamicPattern(importPath); + + optionsImports.push({ + importPath, + resolved: resolveFilename( + loaderContext, + fileResolve, + globResolve, + isGlob, + path.dirname(loaderContext.resourcePath), + importPath + ), + }); + } + + await Promise.all( + optionsImports.map(async (result) => { + const { importPath } = result; + let { resolved } = result; + + try { + resolved = await resolved; + } catch (ignoreError) { + return; + } + + const isArray = Array.isArray(resolved); + + // `stylus` returns forward slashes on windows + // eslint-disable-next-line no-param-reassign + result.resolved = isArray + ? resolved.map((item) => path.normalize(item)) + : path.normalize(resolved); + + resolvedImportDependencies.set(importPath, result); + }) + ); + return class CustomEvaluator extends Evaluator { visitImport(imported) { this.return += 1; @@ -372,64 +414,68 @@ async function createEvaluator(loaderContext, code, options) { let webpackResolveError; - if ( - node.name !== 'url' && - nodePath && - !URL_RE.test(nodePath) && - // `imports` is not resolved, let's avoid extra actions - !this.options.imports.includes(nodePath) - ) { - const dependencies = resolvedDependencies.get( - path.normalize(node.filename) - ); + if (node.name !== 'url' && nodePath && !URL_RE.test(nodePath)) { + let dependency; - if (dependencies) { - const dependency = dependencies.find((item) => { - if ( - item.originalLineno === node.lineno && - item.originalColumn === node.column && - item.originalNodePath === nodePath - ) { - if (item.error) { - webpackResolveError = item.error; - } else { - return item.resolved; - } - } + const isEntrypoint = loaderContext.resourcePath === node.filename; - return false; - }); + if (isEntrypoint) { + dependency = resolvedImportDependencies.get(nodePath); + } - if (dependency) { - const { resolved } = dependency; + if (!dependency) { + const dependencies = resolvedDependencies.get( + path.normalize(node.filename) + ); + + if (dependencies) { + dependency = dependencies.find((item) => { + if ( + item.originalLineno === node.lineno && + item.originalColumn === node.column && + item.originalNodePath === nodePath + ) { + if (item.error) { + webpackResolveError = item.error; + } else { + return item.resolved; + } + } - if (!Array.isArray(resolved)) { - // Avoid re globbing when resolved import contains glob characters - node.string = fastGlob.escapePath(resolved); - } else if (resolved.length > 0) { - let hasError = false; + return false; + }); + } + } - const blocks = resolved.map((item) => { - const clonedImported = imported.clone(); - const clonedNode = this.visit(clonedImported.path).first; + if (dependency) { + const { resolved } = dependency; - // Avoid re globbing when resolved import contains glob characters - clonedNode.string = fastGlob.escapePath(item); + if (!Array.isArray(resolved)) { + // Avoid re globbing when resolved import contains glob characters + node.string = fastGlob.escapePath(resolved); + } else if (resolved.length > 0) { + let hasError = false; - let result; + const blocks = resolved.map((item) => { + const clonedImported = imported.clone(); + const clonedNode = this.visit(clonedImported.path).first; - try { - result = super.visitImport(clonedImported); - } catch (error) { - hasError = true; - } + // Avoid re globbing when resolved import contains glob characters + clonedNode.string = fastGlob.escapePath(item); - return result; - }); + let result; - if (!hasError) { - return mergeBlocks(blocks); + try { + result = super.visitImport(clonedImported); + } catch (error) { + hasError = true; } + + return result; + }); + + if (!hasError) { + return mergeBlocks(blocks); } } } diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index d338ce1..56ea872 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -338,6 +338,20 @@ exports[`loader imports files listed in option argument webpack style: errors 1` exports[`loader imports files listed in option argument webpack style: warnings 1`] = `Array []`; +exports[`loader imports files listed in option argument with tilde: css 1`] = ` +".not-real-nib { + color: #000; +} +.imported-stylus { + border: 5px; +} +" +`; + +exports[`loader imports files listed in option argument with tilde: errors 1`] = `Array []`; + +exports[`loader imports files listed in option argument with tilde: warnings 1`] = `Array []`; + exports[`loader imports files listed in option argument: css 1`] = ` "body { background: url(\\"img.png\\"); @@ -402,6 +416,28 @@ exports[`loader imports files with special characters listed in glob: errors 1`] exports[`loader imports files with special characters listed in glob: warnings 1`] = `Array []`; +exports[`loader imports in option "import" and webpack alias: css 1`] = ` +".webpack-alias-1 { + color: #f00; +} +.webpack-alias-2 { + color: #ff7f50; +} +body { + font: 12px Helvetica, Arial, sans-serif; +} +a.button { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +" +`; + +exports[`loader imports in option "import" and webpack alias: errors 1`] = `Array []`; + +exports[`loader imports in option "import" and webpack alias: warnings 1`] = `Array []`; + exports[`loader imports the right file based on context: css 1`] = ` ".a-color { color: #aaa; diff --git a/test/loader.test.js b/test/loader.test.js index 285f12e..2d6a810 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -708,6 +708,35 @@ describe('loader', () => { expect(getErrors(stats)).toMatchSnapshot('errors'); }); + it('imports files listed in option argument with tilde', async () => { + const testId = './stylus.styl'; + const compiler = getCompiler( + testId, + { + stylusOptions: { + import: ['~fakenib'], + }, + }, + { + resolve: { + modules: ['node_modules'], + }, + } + ); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const codeFromStylus = await getCodeFromStylus(testId, { + stylusOptions: { + import: ['~fakenib'], + }, + }); + + expect(codeFromBundle.css).toBe(codeFromStylus.css); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + it('imports files listed in option "style" package.json', async () => { const testId = './import-fakestylus.styl'; const compiler = getCompiler(testId); @@ -1246,6 +1275,37 @@ describe('loader', () => { expect(getErrors(stats)).toMatchSnapshot('errors'); }); + it('imports in option "import" and webpack alias', async () => { + const testId = './basic.styl'; + const compiler = getCompiler( + testId, + { + stylusOptions: { + import: ['alias/1', '~alias/2'], + }, + }, + { + resolve: { + alias: { + alias: path.resolve(__dirname, 'fixtures', 'alias'), + }, + }, + } + ); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const codeFromStylus = await getCodeFromStylus(testId, { + stylusOptions: { + import: ['alias/1', '~alias/2'], + }, + }); + + expect(codeFromBundle.css).toBe(codeFromStylus.css); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + it('imports the right file based on context', async () => { const testId = './context'; const compiler = getCompiler(testId);