From da566a6da8047c5ffaae8b97f075f896e0152486 Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Tue, 8 Sep 2020 18:38:02 +0300 Subject: [PATCH] feat: add webpack resolver (#231) --- babel.config.js | 2 +- bench/fixtures/imports/webpack.config.js | 17 +- index.js | 221 -------------- package-lock.json | 52 +++- package.json | 2 +- src/evaluator.js | 194 ++++++++++++ src/index.js | 325 ++++++++------------- src/lib/evaluator.js | 166 ----------- src/lib/listimports.js | 80 ----- src/lib/pathcache.js | 304 ------------------- src/lib/resolver.js | 3 +- src/utils.js | 33 +++ test/__snapshots__/listimport.test.js.snap | 31 -- test/__snapshots__/loader.test.js.snap | 11 - test/helpers/getCompiler.js | 27 +- test/helpers/stylus-import-loader.js | 5 - test/listimport.test.js | 71 ----- test/loader.test.js | 27 +- 18 files changed, 433 insertions(+), 1138 deletions(-) delete mode 100644 index.js create mode 100644 src/evaluator.js delete mode 100644 src/lib/evaluator.js delete mode 100644 src/lib/listimports.js delete mode 100644 src/lib/pathcache.js create mode 100644 src/utils.js delete mode 100644 test/__snapshots__/listimport.test.js.snap delete mode 100644 test/helpers/stylus-import-loader.js delete mode 100644 test/listimport.test.js diff --git a/babel.config.js b/babel.config.js index 1b6246b..190c338 100644 --- a/babel.config.js +++ b/babel.config.js @@ -16,4 +16,4 @@ module.exports = (api) => { ], ], }; -}; \ No newline at end of file +}; diff --git a/bench/fixtures/imports/webpack.config.js b/bench/fixtures/imports/webpack.config.js index 8214995..e1717d2 100644 --- a/bench/fixtures/imports/webpack.config.js +++ b/bench/fixtures/imports/webpack.config.js @@ -1,7 +1,7 @@ // Just run "webpack-dev-server" function plugin() { - return function(style) { - style.define('add', function(a, b) { + return (style) => { + style.define('add', (a, b) => { return a.operate('+', b); }); }; @@ -9,21 +9,24 @@ function plugin() { module.exports = { context: __dirname, - entry: "./index.js", + entry: './index.js', output: { - path: __dirname + '/tmp', + path: `${__dirname}/tmp`, filename: 'bundle.js', }, resolve: { - extensions: ["", ".js", ".css", ".styl"] + extensions: ['', '.js', '.css', '.styl'], }, module: { loaders: [ { test: /\.styl$/, - loader: 'style-loader!css-loader!' + require('path').join(__dirname, '../../../index'), + loader: `style-loader!css-loader!${require('path').join( + __dirname, + '../../../index' + )}`, }, - ] + ], }, stylus: { use: [plugin()], diff --git a/index.js b/index.js deleted file mode 100644 index b8941dd..0000000 --- a/index.js +++ /dev/null @@ -1,221 +0,0 @@ -var loaderUtils = require('loader-utils'); -var stylus = require('stylus'); -var path = require('path'); -var fs = require('fs'); -var when = require('when'); -var whenNodefn = require('when/node/function'); -var cloneDeep = require('lodash.clonedeep'); - -var CachedPathEvaluator = require('./lib/evaluator'); -var PathCache = require('./lib/pathcache'); -var resolver = require('./lib/resolver'); - -var globalImportsCaches = {}; -module.exports = function(source) { - var self = this; - this.cacheable && this.cacheable(); - var done = this.async(); - var options = cloneDeep(loaderUtils.getOptions(this) || {}); - options.dest = options.dest || ''; - options.filename = options.filename || this.resourcePath; - options.Evaluator = CachedPathEvaluator; - - var configKey, stylusOptions; - if (this.stylus) { - configKey = options.config || 'default'; - stylusOptions = this.stylus[configKey] || {}; - } else if (this.options) { - configKey = options.config || 'stylus'; - stylusOptions = this.options[configKey] || {}; - } else { - stylusOptions = {}; - } - // Instead of assigning to options, we run them manually later so their side effects apply earlier for - // resolving paths. - var use = options.use || stylusOptions.use || []; - options.import = options.import || stylusOptions.import || []; - options.include = options.include || stylusOptions.include || []; - options.set = options.set || stylusOptions.set || {}; - options.define = options.define || stylusOptions.define || {}; - options.paths = options.paths || stylusOptions.paths; - - if (options.sourceMap != null) { - options.sourcemap = options.sourceMap; - delete options.sourceMap; - } - else if (this.sourceMap) { - options.sourcemap = { comment: false }; - } - - var styl = stylus(source, options); - var paths = [path.dirname(options.filename)]; - - function needsArray(value) { - return (Array.isArray(value)) ? value : [value]; - } - - if (options.paths && !Array.isArray(options.paths)) { - paths = paths.concat(options.paths); - options.paths = [options.paths]; - } - - var manualImports = []; - Object.keys(options).forEach(function(key) { - var value = options[key]; - if (key === 'use') { - needsArray(value).forEach(function(plugin) { - if (typeof plugin === 'function') { - styl.use(plugin); - } else { - throw new Error('Plugin should be a function'); - } - }); - } else if (key === 'set') { - for (var name in value) { - styl.set(name, value[name]); - } - } else if (key === 'define') { - for (var defineName in value) { - styl.define(defineName, value[defineName]); - } - } else if (key === 'include') { - needsArray(value).forEach(styl.include.bind(styl)); - } else if (key === 'import') { - needsArray(value).forEach(function(stylusModule) { - manualImports.push(stylusModule); - }); - } else { - styl.set(key, value); - - if (key === 'resolve url' && value) { - styl.define('url', resolver()); - } - } - }); - - var shouldCacheImports = stylusOptions.importsCache !== false; - - var importsCache; - if (stylusOptions.importsCache !== false) { - if (typeof stylusOptions.importsCache === 'object') { - importsCache = stylusOptions.importsCache; - } else { - if(!globalImportsCaches[configKey]) globalImportsCaches[configKey] = {}; - importsCache = globalImportsCaches[configKey]; - } - } - - // Use input file system's readFile if available. The normal webpack input - // file system is cached with entries purged when they are detected to be - // changed on disk by the watcher. - var readFile; - try { - var inputFileSystem = this._compiler.inputFileSystem; - readFile = inputFileSystem.readFile.bind(inputFileSystem); - } catch (error) { - readFile = fs.readFile; - } - - var boundResolvers = PathCache.resolvers(options, this.resolve); - var pathCacheHelpers = { - resolvers: boundResolvers, - readFile: readFile, - }; - - // Use plugins here so that resolve related side effects can be used while we resolve imports. - (Array.isArray(use) ? use : [use]).forEach(styl.use, styl); - - when - // Resolve manual imports like @import files. - .reduce(manualImports, function resolveManualImports(carry, filename) { - return PathCache.resolvers - .reduce(boundResolvers, path.dirname(options.filename), filename) - .then(function(paths) { return carry.concat(paths); }); - }, []) - // Resolve dependencies of - .then(function(paths) { - paths.forEach(styl.import.bind(styl)); - paths.forEach(self.addDependency); - - var readFile = whenNodefn.lift(pathCacheHelpers.readFile); - return when.reduce(paths, function(cache, filepath) { - return readFile(filepath) - .then(function(source) { - return PathCache.createFromFile( - pathCacheHelpers, cache, source.toString(), filepath - ); - }); - }, { - contexts: {}, - sources: {}, - imports: importsCache, - }); - }) - .then(function(cache) { - return PathCache - .createFromFile(pathCacheHelpers, cache, source, options.filename); - }) - .then(function(importPathCache) { - // CachedPathEvaluator will use this PathCache to find its dependencies. - options.cache = importPathCache; - importPathCache.allDeps().forEach(function(f) { - self.addDependency(path.normalize(f)); - }); - - // var paths = importPathCache.origins; - - styl.render(function(err, css) { - if (err) { - done(err); - } else { - if (styl.sourcemap) { - styl.sourcemap.sourcesContent = styl.sourcemap.sources.map(function (file) { - return importPathCache.sources[path.resolve(file)] - }); - } - done(null, css, styl.sourcemap); - } - }); - }) - .catch(done); -}; - -var LoaderOptionsPlugin = require('webpack').LoaderOptionsPlugin; - -// Webpack 2 plugin for setting options that'll be available to stylus-loader. -function OptionsPlugin(options) { - if (!LoaderOptionsPlugin) { - throw new Error( - 'webpack.LoaderOptionPlugin is not available. A newer version of webpack is needed.' - ); - } - var stylusOptions = {}; - var test = options.test || /\.styl$/; - var include = options.include; - var exclude = options.exclude; - - var loaderOptions = { - stylus: stylusOptions, - }; - for (var key in options) { - if (['test', 'include', 'exclude'].indexOf(key) === -1) { - stylusOptions[key] = options[key]; - } - } - if (test) { - loaderOptions.test = test; - } - if (include) { - loaderOptions.include = include; - } - if (exclude) { - loaderOptions.exclude = exclude; - } - this.plugin = new LoaderOptionsPlugin(loaderOptions); -}; - -module.exports.OptionsPlugin = OptionsPlugin; - -OptionsPlugin.prototype.apply = function(compiler) { - this.plugin.apply(compiler); -}; diff --git a/package-lock.json b/package-lock.json index f225920..0b9f4d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2965,6 +2965,17 @@ "user-meta": "^1.0.0" }, "dependencies": { + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11653,6 +11664,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, "requires": { "minimist": "^1.2.0" } @@ -12051,13 +12063,23 @@ "dev": true }, "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "json5": "^2.1.2" + }, + "dependencies": { + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "requires": { + "minimist": "^1.2.5" + } + } } }, "locate-path": { @@ -17667,6 +17689,17 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -17916,6 +17949,17 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", diff --git a/package.json b/package.json index 48b6d2b..b1c3d2b 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "webpack": "^4.0.0 || ^5.0.0" }, "dependencies": { - "loader-utils": "^1.0.2", + "loader-utils": "^2.0.0", "lodash.clonedeep": "^4.5.0", "schema-utils": "^2.7.1", "when": "~3.6.x" diff --git a/src/evaluator.js b/src/evaluator.js new file mode 100644 index 0000000..0072ca4 --- /dev/null +++ b/src/evaluator.js @@ -0,0 +1,194 @@ +import path from 'path'; + +import Evaluator from 'stylus/lib/visitor/evaluator'; + +import { urlToRequest } from 'loader-utils'; +import Parser from 'stylus/lib/parser'; +import DepsResolver from 'stylus/lib/visitor/deps-resolver'; +import nodes from 'stylus/lib/nodes'; +import utils from 'stylus/lib/utils'; +import { readFile } from './utils'; + +const URL_RE = /^(?:url\s*\(\s*)?['"]?(?:[#/]|(?:https?:)?\/\/)/i; + +async function resolveFilename(filename, currentDirectory, loaderContext) { + const resolve = loaderContext.getResolve({ + conditionNames: ['styl', 'style'], + mainFields: ['styl', 'style', 'main', '...'], + mainFiles: ['index', '...'], + extensions: ['.styl', '.css'], + }); + + const request = urlToRequest( + filename, + // eslint-disable-next-line no-undefined + filename.charAt(0) === '/' ? loaderContext.rootContext : undefined + ); + + return resolveRequests( + currentDirectory, + [...new Set([request, filename])], + resolve + ); +} + +function resolveRequests(context, possibleRequests, resolve) { + if (possibleRequests.length === 0) { + return Promise.reject(); + } + + return resolve(context, possibleRequests[0]) + .then((result) => { + return result; + }) + .catch((error) => { + const [, ...tailPossibleRequests] = possibleRequests; + + if (tailPossibleRequests.length === 0) { + throw error; + } + + return resolveRequests(context, tailPossibleRequests, resolve); + }); +} + +async function getDependencies( + code, + loaderContext, + resolve, + options, + parcelOptions, + seen = new Set() +) { + const filepath = loaderContext.resourcePath; + + seen.add(filepath); + + nodes.filename = filepath; + + const parser = new Parser(code, options); + const ast = parser.parse(); + const deps = new Map(); + + class ImportVisitor extends DepsResolver { + async visitImport(imported) { + const importedPath = imported.path.first.string; + + if (!deps.has(importedPath)) { + deps.set( + importedPath, + resolve(importedPath, loaderContext.rootContext, loaderContext) + ); + } + } + } + + new ImportVisitor(ast, options).visit(ast); + + // Recursively process depdendencies, and return a map with all resolved paths. + const res = new Map(); + + await Promise.all( + Array.from(deps.entries()).map(async ([importedPath, resolved]) => { + try { + resolved = await resolved; + } catch (err) { + resolved = null; + } + + if (typeof importedPath === 'undefined') { + return; + } + + let found; + if (resolved) { + found = Array.isArray(resolved) ? resolved : [resolved]; + res.set(importedPath, resolved); + } else { + // support optional .styl + const originalPath = importedPath; + if (!/\.styl$/i.test(importedPath)) { + importedPath += '.styl'; + } + + const paths = (options.paths || []).concat( + path.dirname(filepath || '.') + ); + found = utils.find(importedPath, paths, filepath); + if (!found) { + found = utils.lookupIndex(originalPath, paths, filepath); + } + + if (!found) { + throw new Error(`failed to locate file ${originalPath}`); + } + } + + // Recursively process resolved files as well to get nested deps + for (const resolved of found) { + if (!seen.has(resolved)) { + // await asset.addIncludedFile({ filePath: resolved }); + + let code; + + try { + code = (await readFile(loaderContext.fs, resolved)).toString(); + } catch (error) { + loaderContext.emitError(error); + } + + for (const [path, resolvedPath] of await getDependencies( + code, + loaderContext, + resolveFilename, + options + )) { + res.set(path, resolvedPath); + } + } + } + }) + ); + + return res; +} + +export default async function createEvaluator(code, options, loaderContext) { + let optionsImports = ''; + + if (options.import) { + optionsImports = options.import + .map((entry) => `@import "${entry}";`) + .join('\n'); + } + + const possibleImports = ( + await Promise.all( + [code, optionsImports].map((code) => + getDependencies(code, loaderContext, resolveFilename, options) + ) + ) + ).reduce((acc, map) => { + acc.push(...map); + return acc; + }, []); + + const deps = new Map(possibleImports); + + return class CustomEvaluator extends Evaluator { + visitImport(imported) { + const node = this.visit(imported.path).first; + const path = node.string; + + if (node.name !== 'url' && path && !URL_RE.test(path)) { + const resolved = deps.get(path); + + if (resolved) { + node.string = resolved; + } + } + + return super.visitImport(imported); + } + }; +} diff --git a/src/index.js b/src/index.js index 66de06c..330a6ec 100644 --- a/src/index.js +++ b/src/index.js @@ -1,220 +1,151 @@ -var loaderUtils = require('loader-utils'); -var stylus = require('stylus'); -var path = require('path'); -var fs = require('fs'); -var when = require('when'); -var whenNodefn = require('when/node/function'); -var cloneDeep = require('lodash.clonedeep'); - -var CachedPathEvaluator = require('./lib/evaluator'); -var PathCache = require('./lib/pathcache'); -var resolver = require('./lib/resolver'); - -var globalImportsCaches = {}; - -export default async function loader(source) { - var self = this; - this.cacheable && this.cacheable(); - var done = this.async(); - var options = cloneDeep(loaderUtils.getOptions(this) || {}); - options.dest = options.dest || ''; +import { promises as fs } from 'fs'; + +import stylus from 'stylus'; + +import createEvaluator from './evaluator'; +import { getOptions, isObject, castArray } from './utils'; +import resolver from './lib/resolver'; + +export default async function stylusLoader(source) { + const callback = this.async(); + + // get options passed to loader + const loaderOptions = getOptions(this); + + // clone loader options to avoid modifying this.query + const options = loaderOptions ? { ...loaderOptions } : {}; + + // access Webpack config + const webpackConfig = + this._compilation && isObject(this._compilation.options) + ? this._compilation.options + : {}; + + // stylus works better with an absolute filename options.filename = options.filename || this.resourcePath; - options.Evaluator = CachedPathEvaluator; - - var configKey, stylusOptions; - if (this.stylus) { - configKey = options.config || 'default'; - stylusOptions = this.stylus[configKey] || {}; - } else if (this.options) { - configKey = options.config || 'stylus'; - stylusOptions = this.options[configKey] || {}; - } else { - stylusOptions = {}; - } - // Instead of assigning to options, we run them manually later so their side effects apply earlier for - // resolving paths. - var use = options.use || stylusOptions.use || []; - options.import = options.import || stylusOptions.import || []; - options.include = options.include || stylusOptions.include || []; - options.set = options.set || stylusOptions.set || {}; - options.define = options.define || stylusOptions.define || {}; - options.paths = options.paths || stylusOptions.paths; + // get sourcemap option in the order: options.sourceMap > options.sourcemap > this.sourceMap if (options.sourceMap != null) { options.sourcemap = options.sourceMap; - delete options.sourceMap; + } else if ( + options.sourcemap == null && + this.sourceMap && + (!webpackConfig.devtool || webpackConfig.devtool.indexOf('eval') !== 0) + ) { + options.sourcemap = {}; } - else if (this.sourceMap) { - options.sourcemap = { comment: false }; - } - - var styl = stylus(source, options); - var paths = [path.dirname(options.filename)]; - function needsArray(value) { - return (Array.isArray(value)) ? value : [value]; - } + // set stylus sourcemap defaults + if (options.sourcemap) { + if (!isObject(options.sourcemap)) { + options.sourcemap = {}; + } - if (options.paths && !Array.isArray(options.paths)) { - paths = paths.concat(options.paths); - options.paths = [options.paths]; + options.sourcemap = Object.assign( + { + // enable loading source map content by default + content: true, + // source map comment is added by css-loader + comment: false, + // set sourceRoot for better handling of paths by css-loader + sourceRoot: this.rootContext, + }, + options.sourcemap + ); } - var manualImports = []; - Object.keys(options).forEach(function(key) { - var value = options[key]; - if (key === 'use') { - needsArray(value).forEach(function(plugin) { - if (typeof plugin === 'function') { - styl.use(plugin); - } else { - throw new Error('Plugin should be a function'); + // create stylus renderer instance + const styl = stylus(source, options); + + // import of plugins passed as strings + if (options.use.length) { + for (const [i, plugin] of Object.entries(options.use)) { + if (typeof plugin === 'string') { + try { + options.use[i] = require(plugin)(); + } catch (err) { + options.use.splice(i, 1); + err.message = `Stylus plugin '${plugin}' failed to load. Are you sure it's installed?`; + this.emitWarning(err); } - }); - } else if (key === 'set') { - for (var name in value) { - styl.set(name, value[name]); - } - } else if (key === 'define') { - for (var defineName in value) { - styl.define(defineName, value[defineName]); - } - } else if (key === 'include') { - needsArray(value).forEach(styl.include.bind(styl)); - } else if (key === 'import') { - needsArray(value).forEach(function(stylusModule) { - manualImports.push(stylusModule); - }); - } else { - styl.set(key, value); - - if (key === 'resolve url' && value) { - styl.define('url', resolver()); } } - }); - - var shouldCacheImports = stylusOptions.importsCache !== false; - - var importsCache; - if (stylusOptions.importsCache !== false) { - if (typeof stylusOptions.importsCache === 'object') { - importsCache = stylusOptions.importsCache; - } else { - if(!globalImportsCaches[configKey]) globalImportsCaches[configKey] = {}; - importsCache = globalImportsCaches[configKey]; - } } - // Use input file system's readFile if available. The normal webpack input - // file system is cached with entries purged when they are detected to be - // changed on disk by the watcher. - var readFile; - try { - var inputFileSystem = this._compiler.inputFileSystem; - readFile = inputFileSystem.readFile.bind(inputFileSystem); - } catch (error) { - readFile = fs.readFile; + // add custom include paths + if ('include' in options) { + castArray(options.include).forEach(styl.include, styl); } - var boundResolvers = PathCache.resolvers(options, this.resolve); - var pathCacheHelpers = { - resolvers: boundResolvers, - readFile: readFile, - }; - - // Use plugins here so that resolve related side effects can be used while we resolve imports. - (Array.isArray(use) ? use : [use]).forEach(styl.use, styl); - - when - // Resolve manual imports like @import files. - .reduce(manualImports, function resolveManualImports(carry, filename) { - return PathCache.resolvers - .reduce(boundResolvers, path.dirname(options.filename), filename) - .then(function(paths) { return carry.concat(paths); }); - }, []) - // Resolve dependencies of - .then(function(paths) { - paths.forEach(styl.import.bind(styl)); - paths.forEach(self.addDependency); - - var readFile = whenNodefn.lift(pathCacheHelpers.readFile); - return when.reduce(paths, function(cache, filepath) { - return readFile(filepath) - .then(function(source) { - return PathCache.createFromFile( - pathCacheHelpers, cache, source.toString(), filepath - ); - }); - }, { - contexts: {}, - sources: {}, - imports: importsCache, - }); - }) - .then(function(cache) { - return PathCache - .createFromFile(pathCacheHelpers, cache, source, options.filename); - }) - .then(function(importPathCache) { - // CachedPathEvaluator will use this PathCache to find its dependencies. - options.cache = importPathCache; - importPathCache.allDeps().forEach(function(f) { - self.addDependency(path.normalize(f)); - }); - - // var paths = importPathCache.origins; - - styl.render(function(err, css) { - if (err) { - done(err); - } else { - if (styl.sourcemap) { - styl.sourcemap.sourcesContent = styl.sourcemap.sources.map(function (file) { - return importPathCache.sources[path.resolve(file)] - }); - } - done(null, css, styl.sourcemap); - } - }); - }) - .catch(done); -}; - -var LoaderOptionsPlugin = require('webpack').LoaderOptionsPlugin; - -// Webpack 2 plugin for setting options that'll be available to stylus-loader. -export function OptionsPlugin(options) { - if (!LoaderOptionsPlugin) { - throw new Error( - 'webpack.LoaderOptionPlugin is not available. A newer version of webpack is needed.' - ); + // add custom stylus file imports + if ('import' in options) { + castArray(options.import).forEach(styl.import, styl); } - var stylusOptions = {}; - var test = options.test || /\.styl$/; - var include = options.include; - var exclude = options.exclude; - - var loaderOptions = { - stylus: stylusOptions, - }; - for (var key in options) { - if (['test', 'include', 'exclude'].indexOf(key) === -1) { - stylusOptions[key] = options[key]; + + // enable resolver for relative urls + if (options.resolveUrl) { + if (!isObject(options.resolveUrl)) { + options.resolveUrl = {}; } + + styl.define('url', resolver(options)); } - if (test) { - loaderOptions.test = test; + + // define global variables/functions + if (isObject(options.define)) { + const raw = options.defineRaw == null ? true : options.defineRaw; + + for (const entry of Object.entries(options.define)) { + styl.define(...entry, raw); + } } - if (include) { - loaderOptions.include = include; + + // include regular CSS on @import + if (options.includeCSS) { + styl.set('include css', true); } - if (exclude) { - loaderOptions.exclude = exclude; + + styl.set('Evaluator', await createEvaluator(source, options, this)); + + // keep track of imported files (used by Stylus CLI watch mode) + options._imports = []; + + // trigger callback before compiling + if (typeof options.beforeCompile === 'function') { + options.beforeCompile(styl, this, options); } - this.plugin = new LoaderOptionsPlugin(loaderOptions); -}; -OptionsPlugin.prototype.apply = function(compiler) { - this.plugin.apply(compiler); -}; + // let stylus do its magic + styl.render(async (err, css) => { + if (err) { + this.addDependency(err.filename); + return callback(err); + } + + // add all source files as dependencies + if (options._imports.length) { + for (const importData of options._imports) { + this.addDependency(importData.path); + } + } + + if (styl.sourcemap) { + // css-loader will set the source map file name + delete styl.sourcemap.file; + + // load source file contents into source map + if (options.sourcemap && options.sourcemap.content) { + try { + styl.sourcemap.sourcesContent = await Promise.all( + styl.sourcemap.sources.map((file) => fs.readFile(file, 'utf-8')) + ); + } catch (e) { + return callback(e); + } + } + } + + // profit + callback(null, css, styl.sourcemap); + }); +} diff --git a/src/lib/evaluator.js b/src/lib/evaluator.js deleted file mode 100644 index dc6013d..0000000 --- a/src/lib/evaluator.js +++ /dev/null @@ -1,166 +0,0 @@ -// Except for a block in #visitImport, everything here is an exact copy of the -// source in stylus this currently depends on. If stylus is updated, update -// this appropriately. - -var Evaluator = require('stylus/lib/visitor/evaluator') - , nodes = require('stylus/lib/nodes') - , Stack = require('stylus/lib/stack') - , Frame = require('stylus/lib/stack/frame') - , Scope = require('stylus/lib/stack/scope') - , utils = require('stylus/lib/utils') - , bifs = require('stylus/lib/functions') - , basename = require('path').basename - , dirname = require('path').dirname - , relative = require('path').relative - , join = require('path').join - , colors = require('stylus/lib/colors') - // , debug = require('debug')('stylus:evaluator') - , fs = require('fs'); - -module.exports = CachedPathEvaluator; - -/** - * Import `file` and return Block node. - * - * @api private - */ -function importFile(node, file, literal, index) { - var importStack = this.importStack - , Parser = require('stylus/lib/parser') - , stat; - - // Handling the `require` - if (node.once) { - if (this.requireHistory[file]) return nodes.null; - this.requireHistory[file] = true; - - if (literal && !this.includeCSS) { - return node; - } - } - - // Expose imports - node.path = file; - node.dirname = dirname(file); - // Store the modified time - stat = fs.statSync(file); - node.mtime = stat.mtime; - this.paths.push(node.dirname); - - // Avoid overflows from importing the same file over again - if (file === importStack[importStack.length - 1]) return nodes.null; - - if (this.options._imports) this.options._imports.push(node.clone()); - - // Parse the file - importStack.push(file); - nodes.filename = file; - - var str; - if (this.cache.sources && this.cache.sources[file]) { - str = this.cache.sources[file]; - } else { - str = fs.readFileSync(file, 'utf8'); - } - - if (literal && !this.resolveURL) return new nodes.Literal(str.replace(/\r\n?/g, '\n')); - - // parse - var block = new nodes.Block - , parser = new Parser(str, utils.merge({ root: block }, this.options)); - - try { - block = parser.parse(); - } catch (err) { - err.filename = file; - err.lineno = parser.lexer.lineno; - err.input = str; - throw err; - } - - // Evaluate imported "root" - block.parent = this.root; - block.scope = false; - var ret = this.visit(block); - importStack.pop(); - if (importStack.length || index) this.paths.pop(); - - return ret; -} - -function CachedPathEvaluator(root, options) { - Evaluator.apply(this, arguments); - - this.cache = options.cache; -} - -CachedPathEvaluator.prototype = Object.create(Evaluator.prototype); -CachedPathEvaluator.prototype.constructor = CachedPathEvaluator; - -CachedPathEvaluator.prototype.visitImport = function(imported) { - this.return++; - - var path = this.visit(imported.path).first - , nodeName = imported.once ? 'require' : 'import' - , found - , literal - , index; - - this.return--; - // debug('import %s', path); - - // url() passed - if ('url' == path.name) { - if (imported.once) throw new Error('You cannot @require a url'); - - return imported; - } - - // Ensure string - if (!path.string) throw new Error('@' + nodeName + ' string expected'); - - var name = path = path.string; - - // Absolute URL - if (/url\s*\(\s*['"]?(?:https?:)?\/\//i.test(path)) { - if (imported.once) throw new Error('You cannot @require a url'); - return imported; - } - - // Literal - if (/\.css(?:"|$)/.test(path)) { - literal = true; - if (!imported.once && !this.includeCSS) { - return imported; - } - } - - // support optional .styl - if (!literal && !/\.styl$/i.test(path)) path += '.styl'; - - /***************************************************************************** - * THIS IS THE ONLY BLOCK THAT DIFFERS FROM THE ACTUAL STYLUS IMPLEMENTATION. * - *****************************************************************************/ - // Lookup - var dirname = this.paths[this.paths.length - 1]; - found = this.cache.find(path, dirname); - index = this.cache.isIndex(path, dirname); - if (!found) { - found = utils.find(path, this.paths, this.filename); - if (!found) { - found = utils.lookupIndex(name, this.paths, this.filename); - index = true; - } - } - - // Throw if import failed - if (!found) throw new Error('failed to locate @' + nodeName + ' file ' + path); - - var block = new nodes.Block; - - for (var i = 0, len = found.length; i < len; ++i) { - block.push(importFile.call(this, imported, found[i], literal, index)); - } - - return block; -} diff --git a/src/lib/listimports.js b/src/lib/listimports.js deleted file mode 100644 index bcf9540..0000000 --- a/src/lib/listimports.js +++ /dev/null @@ -1,80 +0,0 @@ -var Parser = require('stylus/lib/parser'); -var Visitor = require('stylus/lib/visitor'); -var nodes = require('stylus/lib/nodes'); - -module.exports = listImports; - -// ImportVisitor is a simple stylus ast visitor that navigates the graph -// building a list of imports in it. -function ImportVisitor() { - Visitor.apply(this, arguments); - this.importPaths = []; -} - -ImportVisitor.prototype = Object.create(Visitor.prototype); -ImportVisitor.prototype.constructor = ImportVisitor; - -ImportVisitor.prototype.visitImport = function(node) { - this.importPaths.push(node.path.first.string); - return node; -}; - -ImportVisitor.prototype.visitRoot = function(block){ - for (var i = 0; i < block.nodes.length; ++i) { - this.visit(block.nodes[i]); - } - return block; -}; - -ImportVisitor.prototype.visitExpression = function(expr) { - for (var i = 0; i < expr.nodes.length; ++i) { - this.visit(expr.nodes[i]); - } - return expr; -}; - -ImportVisitor.prototype.visitCall = function(fn) { - if (fn.name === 'use' || fn.name === 'json') { - this.importPaths.push(fn.args.first.string); - } - return fn; -}; - -ImportVisitor.prototype.visitSelector = function(sel) { - for (var i = 0; i < sel.block.nodes.length; i++) { - this.visit(sel.block.nodes[i]); - } - return sel; -} - -ImportVisitor.prototype.visitBlock = ImportVisitor.prototype.visitRoot; -ImportVisitor.prototype.visitGroup = ImportVisitor.prototype.visitRoot; - -// Returns a list of paths that given source imports. -function listImports(source, options) { - // Store source -> imports work in a cache. The Parser is the most expensive - // part of stylus and we can't use their cache without creating undesired side - // effects later during the actual render. In single run builds this will - // benefit repeated files imported like common styling. In multiple run builds - // this will help stylus import trees when a dependency changes, the higher up - // files won't need to be parsed again. - var cache = options.cache; - if (cache && cache[source]) { return cache[source]; } - - // Current idea here is to silence errors and let them rise in stylus's - // renderer which has more handling so that the error message is more - // meaningful and easy to understand. - try { - var ast = new Parser(source, { cache: false }).parse(); - } catch (e) { - return []; - } - var importVisitor = new ImportVisitor(ast, {}); - importVisitor.visit(ast); - - if (cache) { - cache[source] = importVisitor.importPaths; - } - - return importVisitor.importPaths; -} diff --git a/src/lib/pathcache.js b/src/lib/pathcache.js deleted file mode 100644 index 5437cef..0000000 --- a/src/lib/pathcache.js +++ /dev/null @@ -1,304 +0,0 @@ -var path = require('path'); -var fs = require('fs'); - -var Evaluator = require('stylus/lib/visitor/evaluator'); -var loaderUtils = require('loader-utils'); -var nodes = require('stylus/lib/nodes'); -var utils = require('stylus/lib/utils'); -var when = require('when'); -var whenNodefn = require('when/node/function'); - -var listImports = require('./listimports'); - -module.exports = PathCache; - -var readFile = whenNodefn.lift(fs.readFile); - -// A cache of import paths of a stylus file resolved to their location on disk -// before the stylus file is rendered. With a special evaluator this lets us -// webpack's resolver. -function PathCache(contexts, sources, imports) { - this.contexts = contexts; - this.sources = sources; - this.imports = imports; - - // Non relative paths are simpler and looked up in this as a fallback - // to this.context. - this.simpleContext = {}; - for (var dirname in this.contexts) { - for (var path in this.contexts[dirname]) { - this.simpleContext[path] = this.contexts[dirname][path]; - } - } -} - -// Return a promise for a PathCache. -PathCache.create = function(contexts, sources, imports) { - return when(new PathCache(contexts, sources, imports)); -}; -PathCache.createFromFile = resolveFileDeep; - -// Create a list of ways to resolve paths. -PathCache.resolvers = resolvers; - -PathCache.resolvers.reduce = reduceResolvers; - -// Lookup the path in this cache. -PathCache.prototype.find = function(path, dirname) { - if (this.contexts[dirname] && this.contexts[dirname][path]) { - return this.contexts[dirname][path].path; - } else if (this.simpleContext[path]) { - return this.simpleContext[path].path; - } else if (/.styl$/.test(path)) { - // A user can specify @import 'something.styl' but if they specify - // @import 'something' stylus adds .styl, we drop that here to see if we - // looked for it without .styl. - return this.find(path.replace(/.styl$/, ''), dirname); - } else { - return undefined; - } -}; - -// Return if the path in this cache is an index file. -PathCache.prototype.isIndex = function(path, dirname) { - if (this.contexts[dirname] && this.contexts[dirname][path]) { - return this.contexts[dirname][path].index; - } else { - return undefined; - } -}; - -// Return an array of all imports the original file depends on. -PathCache.prototype.allDeps = function() { - var deps = []; - for (var dirname in this.contexts) { - for (var path in this.contexts[dirname]) { - if (this.contexts[dirname][path]) { - deps = deps.concat(this.contexts[dirname][path].path); - } - } - } - return deps; -}; - -// Create an array of ways to resolve a path. -// -// The resolved paths may be a path or an object specifying path and index -// members. The index member is used later by stylus, we store it at this point. -function resolvers(options, webpackResolver) { - var evaluator = new Evaluator(nodes.null, options); - var whenWebpackResolver = whenNodefn.lift(webpackResolver); - - // Stylus's normal resolver for single files. - var stylusFile = function(context, path) { - // Stylus adds .styl to paths for normal "paths" lookup if it isn't there. - if (!/.styl$/.test(path)) { - path += '.styl'; - } - - var paths = options.paths.concat(context); - var found = utils.find(path, paths, options.filename) - if (found) { - return normalizePaths(found); - } - }; - - // Stylus's normal resolver for node_modules packages. Cannot locate paths - // inside a package. - var stylusIndex = function(context, path) { - // Stylus calls the argument name. If it exists it should match the name - // of a module in node_modules. - if (!path) { - return null; - } - - var paths = options.paths.concat(context); - var found = utils.lookupIndex(path, paths, options.filename); - if (found) { - return {path: normalizePaths(found), index: true}; - } - }; - - // Fallback to resolving with webpack's configured resovler. - var webpackResolve = function(context, path) { - // Follow the webpack stylesheet idiom of '~path' meaning a path in a - // modules folder and a unprefixed 'path' meaning a relative path like - // './path'. - path = loaderUtils.urlToRequest(path, options.root); - // First try with a '.styl' extension. - return whenWebpackResolver(context, path + '.styl') - // If the user adds ".styl" to resolve.extensions, webpack can find - // index files like stylus but it uses all of webpack's configuration, - // by default for example the module could be web_modules. - .catch(function() { return whenWebpackResolver(context, path); }) - .catch(function() { return null; }) - .then(function(result) { - return Array.isArray(result) && result[1] && result[1].path || result - }); - }; - - if (options.preferPathResolver === 'webpack') { - return [ - webpackResolve, - stylusFile, - stylusIndex - ]; - } - else { - return [ - stylusFile, - stylusIndex, - webpackResolve - ]; - } -} - -function reduceResolvers(resolvers, context, path) { - return when - .reduce(resolvers, function(result, resolver) { - return result ? result : resolver(context, path); - }, undefined); -} - -// Run resolvers on one path and return an object with the found path under a -// key of the original path. -// -// Example: -// resolving the path -// 'a/file' -// returns an object -// {'a/file': {path: ['node_modules/a/file'], index: true}} -function resolveOne(resolvers, context, path) { - return reduceResolvers(resolvers, context, path) - .then(function(result) { - result = typeof result === 'string' ? [result] : result; - result = Array.isArray(result) ? {path: result, index: false} : result; - var map = {}; - map[path] = result; - return map; - }); -} - -// Run the resolvers on an array of paths and return an object like resolveOne. -function resolveMany(resolvers, context, paths) { - return when - .map(paths, resolveOne.bind(null, resolvers, context)) - .then(function(maps) { - return maps.reduce(function(map, resolvedPaths) { - Object.keys(resolvedPaths).forEach(function(path) { - map[path] = resolvedPaths[path]; - }); - return map; - }, {}); - }); -} - -// Load a file at fullPath, resolve all of it's imports and report for those. -function resolveFileDeep(helpers, parentCache, source, fullPath) { - var resolvers = helpers.resolvers; - var readFile = helpers.readFile; - - var contexts = parentCache.contexts; - var sources = parentCache.sources; - - contexts = contexts || {}; - var nestResolve = resolveFileDeep.bind(null, helpers, parentCache, null); - var context = path.dirname(fullPath); - readFile = whenNodefn.lift(readFile); - - return when - .resolve(source || sources[fullPath] || readFile(fullPath)) - // Cast the buffer from the cached input file system to a string. - .then(String) - // Store the source so that the evaluator doesn't need to touch the - // file system. - .then(function(_source) { - sources[fullPath] = _source; - return _source; - }) - // Make sure the stylus functions/index.styl source is stored. - .then(partial(ensureFunctionsSource, sources)) - // List imports and use its cache. The source file is translated into a - // list of imports. Where the source file came from isn't important for the - // list. The where is added by resolveMany with the context and resolvers. - .then(partialRight(listImports, { cache: parentCache.imports })) - .then(resolveMany.bind(null, resolvers, context)) - .then(function(newPaths) { - // Contexts are the full path since multiple could be in the same folder - // but different deps. - contexts[context] = merge(contexts[context] || {}, newPaths); - return when.map(Object.keys(newPaths), function(key) { - var found = newPaths[key] && newPaths[key].path; - if (found) { - return when.map(found, nestResolve); - } - }); - }) - .then(function() { - return PathCache.create(contexts, sources, parentCache.imports); - }); -} - -// Resolve functions in a promise wrapper to catch any errors from resolving. -var functionsPath = - new when.Promise(function(resolve) { - resolve(require.resolve('stylus/lib/functions/index.styl')); - }) - .catch(function() { return ''; }); - -var functionsSource = functionsPath - .then(readFile) - .catch(function(error) { - // Ignore error if functions/index.styl doesn't exist. - if (error.code !== 'ENOENT') { - throw error; - } - return ''; - }) - .then(String); - -function ensureFunctionsSource(sources, source) { - if (!sources[functionsPath]) { - return functionsSource - .then(function(functionsSource) { - if (functionsSource) { - sources[functionsPath] = functionsSource; - } - }) - // Pass through the source given to this function. - .yield(source); - } - // Pass through the source given to this function. - return source; -} - -var slice = Array.prototype.slice.call.bind(Array.prototype.slice); - -function merge(a, b) { - var key; - for (key in b) { - a[key] = b[key]; - } - return a; -} - -function partial(fn) { - var args = slice(arguments, 1); - return function() { - return fn.apply(this, args.concat(slice(arguments))); - }; -} - -function partialRight(fn) { - var args = slice(arguments, 1); - return function() { - return fn.apply(this, slice(arguments).concat(args)); - }; -} - -function normalizePaths(paths) { - for(var i in paths) { - paths[i] = path.normalize(paths[i]); - } - return paths; -} \ No newline at end of file diff --git a/src/lib/resolver.js b/src/lib/resolver.js index c4cb078..55c80cd 100644 --- a/src/lib/resolver.js +++ b/src/lib/resolver.js @@ -27,7 +27,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*jshint laxcomma:true */ +/* eslint-disable */ var Stylus = require('stylus') , Compiler = Stylus.Compiler , nodes = Stylus.nodes @@ -110,3 +110,4 @@ module.exports = function(options) { url.raw = true; return url; }; +/* eslint-enable */ diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..5bfc7a7 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,33 @@ +export function getOptions(context) { + if (typeof context.getOptions === 'function') { + return context.getOptions(); + } + + // eslint-disable-next-line global-require + return require('loader-utils').getOptions(context); +} + +export function isObject(value) { + return typeof value === 'object' && value !== null; +} + +export function castArray(value) { + if (value == null) { + return []; + } else if (Array.isArray(value)) { + return value; + } + + return [value]; +} + +export function readFile(inputFileSystem, path) { + return new Promise((resolve, reject) => { + inputFileSystem.readFile(path, (err, stats) => { + if (err) { + reject(err); + } + resolve(stats); + }); + }); +} diff --git a/test/__snapshots__/listimport.test.js.snap b/test/__snapshots__/listimport.test.js.snap deleted file mode 100644 index 2552205..0000000 --- a/test/__snapshots__/listimport.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`listimport recognizes @import: css 1`] = `"module.exports = [\\"local-file.styl\\"];"`; - -exports[`listimport recognizes @import: errors 1`] = `Array []`; - -exports[`listimport recognizes @import: warnings 1`] = `Array []`; - -exports[`listimport recognizes @require: css 1`] = `"module.exports = [\\"local-file.styl\\"];"`; - -exports[`listimport recognizes @require: errors 1`] = `Array []`; - -exports[`listimport recognizes @require: warnings 1`] = `Array []`; - -exports[`listimport recognizes block-level imports: css 1`] = `"module.exports = [\\"outside-block.styl\\",\\"inside-block.styl\\",\\"nested-inside-block.styl\\"];"`; - -exports[`listimport recognizes block-level imports: errors 1`] = `Array []`; - -exports[`listimport recognizes block-level imports: warnings 1`] = `Array []`; - -exports[`listimport recognizes json(): css 1`] = `"module.exports = [\\"neat-json.json\\"];"`; - -exports[`listimport recognizes json(): errors 1`] = `Array []`; - -exports[`listimport recognizes json(): warnings 1`] = `Array []`; - -exports[`listimport recognizes use(): css 1`] = `"module.exports = [\\"neat-functions.js\\"];"`; - -exports[`listimport recognizes use(): errors 1`] = `Array []`; - -exports[`listimport recognizes use(): warnings 1`] = `Array []`; diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index 77f2366..c28782f 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -17,17 +17,6 @@ exports[`loader correctly compiles mixin calls inside imported files: errors 1`] exports[`loader correctly compiles mixin calls inside imported files: warnings 1`] = `Array []`; -exports[`loader imports files in a directory included by a plugin: css 1`] = ` -".in-include { - content: 'plugin'; -} -" -`; - -exports[`loader imports files in a directory included by a plugin: errors 1`] = `Array []`; - -exports[`loader imports files in a directory included by a plugin: warnings 1`] = `Array []`; - exports[`loader imports files listed in option argument and deps: css 1`] = ` ".imported-stylus { border: 5px; diff --git a/test/helpers/getCompiler.js b/test/helpers/getCompiler.js index 97452c7..bc225a3 100644 --- a/test/helpers/getCompiler.js +++ b/test/helpers/getCompiler.js @@ -3,8 +3,6 @@ import path from 'path'; import webpack from 'webpack'; import { createFsFromVolume, Volume } from 'memfs'; -import { OptionsPlugin } from '../../src'; - export default (fixture, loaderOptions = {}, config = {}) => { const fullConfig = { mode: 'development', @@ -33,19 +31,16 @@ export default (fixture, loaderOptions = {}, config = {}) => { }, ], }, - plugins: [ - new OptionsPlugin({ - default: { - use: [plugin(), includePlugin()], - }, - }), - ], + plugins: [], resolve: { extensions: ['.js', '.css', '.styl'], modules: [ 'node_modules', path.join(__dirname, '../', 'fixtures', 'web_modules'), ], + alias: { + '~in-web-modules': path.join(__dirname, '../', 'fixtures', 'web_modules'), + }, }, ...config, }; @@ -62,17 +57,3 @@ export default (fixture, loaderOptions = {}, config = {}) => { return compiler; }; - -function plugin() { - return function (style) { - style.define('add', function (a, b) { - return a.operate('+', b); - }); - }; -} - -function includePlugin() { - return function (style) { - style.include(path.join(__dirname, '../', 'fixtures', 'include')); - }; -} diff --git a/test/helpers/stylus-import-loader.js b/test/helpers/stylus-import-loader.js deleted file mode 100644 index 1947a33..0000000 --- a/test/helpers/stylus-import-loader.js +++ /dev/null @@ -1,5 +0,0 @@ -const listImports = require('../../src/lib/listimports'); - -module.exports = function(content) { - return 'module.exports = ' + JSON.stringify(listImports(content, {cache: {}})) + ';'; -}; diff --git a/test/listimport.test.js b/test/listimport.test.js deleted file mode 100644 index 321baa6..0000000 --- a/test/listimport.test.js +++ /dev/null @@ -1,71 +0,0 @@ -import path from 'path'; - -import { - compile, - getCodeFromBundle, - getCompiler, - getErrors, - getWarnings, -} from './helpers'; - -describe('listimport', () => { - const cases = [ - { - name: 'recognizes @import', - asset: './imports/import.styl', - }, - { - name: 'recognizes @require', - asset: './imports/require.styl', - }, - { - name: 'recognizes use()', - asset: './imports/use.styl', - }, - { - name: 'recognizes json()', - asset: './imports/json.styl', - }, - { - name: 'recognizes block-level imports', - asset: './imports/block-level.styl', - }, - ]; - - for (const item of cases) { - it(item.name, async () => { - const testId = item.asset; - const compiler = getCompiler( - testId, - {}, - { - module: { - rules: [ - { - test: /\.styl$/i, - rules: [ - { - loader: require.resolve('./helpers/testLoader'), - }, - { - loader: path.resolve( - __dirname, - './helpers/stylus-import-loader' - ), - options: {}, - }, - ], - }, - ], - }, - } - ); - const stats = await compile(compiler); - const codeFromBundle = getCodeFromBundle(stats, compiler); - - expect(codeFromBundle.css).toMatchSnapshot('css'); - expect(getWarnings(stats)).toMatchSnapshot('warnings'); - expect(getErrors(stats)).toMatchSnapshot('errors'); - }); - } -}); diff --git a/test/loader.test.js b/test/loader.test.js index c38bebf..758d021 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -53,7 +53,7 @@ describe('loader', () => { it('with option, should resolve urls relatively', async () => { const testId = './shallow.styl'; - const compiler = getCompiler(testId, { 'resolve url': true }); + const compiler = getCompiler(testId, { resolveUrl: true }); const stats = await compile(compiler); const codeFromBundle = getCodeFromBundle(stats, compiler); @@ -64,7 +64,7 @@ describe('loader', () => { it('with paths, find deps and load like normal stylus', async () => { const testId = './import-paths.styl'; - const compiler = getCompiler(testId, { paths: 'test/fixtures/paths' }); + const compiler = getCompiler(testId, { paths: ['test/fixtures/paths'] }); const stats = await compile(compiler); const codeFromBundle = getCodeFromBundle(stats, compiler); @@ -97,7 +97,7 @@ describe('loader', () => { it('in a nested import load module from paths', async () => { const testId = './shallow-paths.styl'; - const compiler = getCompiler(testId, { paths: 'test/fixtures/paths' }); + const compiler = getCompiler(testId, { paths: ['test/fixtures/paths'] }); const stats = await compile(compiler); const codeFromBundle = getCodeFromBundle(stats, compiler); @@ -151,8 +151,16 @@ describe('loader', () => { }); it('should allow stylus plugins to be configured in webpack.config.js', async () => { + function plugin() { + return (style) => { + style.define('add', (a, b) => { + return a.operate('+', b); + }); + }; + } + const testId = './webpack.config-plugin.styl'; - const compiler = getCompiler(testId); + const compiler = getCompiler(testId, { use: [plugin()] }); const stats = await compile(compiler); const codeFromBundle = getCodeFromBundle(stats, compiler); @@ -194,17 +202,6 @@ describe('loader', () => { expect(getErrors(stats)).toMatchSnapshot('errors'); }); - it('imports files in a directory included by a plugin', async () => { - const testId = './import-include.styl'; - const compiler = getCompiler(testId); - const stats = await compile(compiler); - const codeFromBundle = getCodeFromBundle(stats, compiler); - - expect(codeFromBundle.css).toMatchSnapshot('css'); - expect(getWarnings(stats)).toMatchSnapshot('warnings'); - expect(getErrors(stats)).toMatchSnapshot('errors'); - }); - it('imports files listed in option argument stylus paths style', async () => { const testId = './stylus.styl'; const compiler = getCompiler(testId, {