From 21be305d853d499a2bc250fecf6d44b8c908599a Mon Sep 17 00:00:00 2001 From: Ben Holloway Date: Wed, 24 Mar 2021 22:28:26 +1100 Subject: [PATCH 1/5] refer to the changelog in the main readme --- packages/resolve-url-loader/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/resolve-url-loader/README.md b/packages/resolve-url-loader/README.md index 53e63d3..ed11dd9 100644 --- a/packages/resolve-url-loader/README.md +++ b/packages/resolve-url-loader/README.md @@ -59,6 +59,8 @@ With functions and mixins and multiple nesting it gets more complicated. Read mo ## Getting started +> **Upgrading?** the [changelog](CHANGELOG.md) shows how to migrate your webpack config. + ### Install via npm From 4dac73a3415d80970e006f08b52ee199436fcb16 Mon Sep 17 00:00:00 2001 From: Ben Holloway Date: Wed, 17 Mar 2021 17:42:35 +1100 Subject: [PATCH 2/5] refactor join API again, additional how-to examples --- .jshintrc | 2 +- .../docs/advanced-features.md | 420 +++++++--- packages/resolve-url-loader/index.js | 33 +- .../resolve-url-loader/lib/engine/rework.js | 2 +- .../lib/join-function/debug.js | 95 ++- .../lib/join-function/debug.test.js | 242 ++++++ .../lib/join-function/fs-utils.js | 24 + .../lib/join-function/index.js | 376 +++++---- .../lib/join-function/index.test.js | 756 +++++++++--------- .../lib/join-function/sanitise-iterable.js | 34 - .../resolve-url-loader/lib/value-processor.js | 50 +- packages/resolve-url-loader/package.json | 3 +- 12 files changed, 1263 insertions(+), 774 deletions(-) create mode 100644 packages/resolve-url-loader/lib/join-function/debug.test.js create mode 100644 packages/resolve-url-loader/lib/join-function/fs-utils.js delete mode 100644 packages/resolve-url-loader/lib/join-function/sanitise-iterable.js diff --git a/.jshintrc b/.jshintrc index ae39197..e8b1ba9 100644 --- a/.jshintrc +++ b/.jshintrc @@ -23,5 +23,5 @@ "maxlen": 120, "scripturl": true, "node": true, - "esversion": 6 + "esversion": 9 } \ No newline at end of file diff --git a/packages/resolve-url-loader/docs/advanced-features.md b/packages/resolve-url-loader/docs/advanced-features.md index 5755edf..ebc4038 100644 --- a/packages/resolve-url-loader/docs/advanced-features.md +++ b/packages/resolve-url-loader/docs/advanced-features.md @@ -1,223 +1,397 @@ # Advanced Features -⚠️ **IMPORTANT** - First read how the [algorithm](./how-it-works.md#algorithm) works. - -* [building blocks](#building-blocks) -* [reference](#reference) -* [how to](#how-to) - All the advanced features of this loader involve customising the `join` option. -The "join" function determines how CSS URIs are combined with one of the possible base paths the loader has identified. - -The "join" function is higher-order with several layers of currying so it is non-trivial to originate from scratch. It is best created from its constituent **generator** and **operation** using the supplied `createJoinFunction` utility. - -## Building blocks - -A number of building blocks are conveniently re-exported as properties of the loader. - -This includes `createJoinFunction`, a simplified means to create a "join" function from a `generator` and an `operation`. +Jump to the **"how to"** section - + * [How to: change precedence of source locations](#how-to-change-precedence-of-source-locations) + * [How to: fallback to a theme or other global directory](#how-to-fallback-to-a-theme-or-other-global-directory) + * [How to: fallback to some other asset file](#how-to-fallback-to-some-other-asset-file) + * [How to: perform a file-system search for an asset](#how-to-perform-a-file-system-search-for-an-asset) -1. The `generator` orders potential base paths to consider. It can do so lazily. -2. The `operation` considers single base path, joins it with the original URI, and determines success. +## What is the "join" function? -When **not** specifying the `join` option, the in-built `defaultJoin` has the following behaviour. +The "join" function determines how CSS URIs are combined with one of the possible base paths the algorithm has identified. -1. A **generator** constructs an Array or iterator of base paths. - - For relative URIs it uses the source-map locations in order `subString`, `value`, `property`, `selector` per the [algorithm](./how-it-works.md#algorithm). +⚠️ **IMPORTANT** - First read how the [algorithm](./how-it-works.md#algorithm) works. - For absolute URIs it uses single element, that being the loader `root` option. +The "join" function is higher-order function created using the `options` and `loader` reference. That gives a function that accepts a single `item` and synchronously returns an absolute asset path to substitute back into the original CSS. - This is the `defaultJoinGenerator`. - -2. The iterator is accessed sequentially, and a single base path is considered. +```javascript +(options:{}, loader:{}) => + (item:{ uri:string, query: string, isAbsolute: boolean, bases:{} }) => + string | null +``` - For each base path we perform the **operation**. - * The base path is joined with the URI to create an absolute file path. - * The webpack file-system is checked for the existance of that file. - * If the file exists we break iteration and it becomes the result. - * If the file does **not** exist it becomes the **fallback** result. +Where the `bases` are absolute directory paths `{ subString, value, property, selector }` per the [algorithm](./how-it-works.md#algorithm). Note that returning `null` implies no substitution, the original relative `uri` is retained. - The **fallback** value will appear in the webpack error message when no valid solutions exist. Only the earliest fallback is ever used since that is considered the most likely base path to resolve. +The job of the "join" function is to consider possible locations for the asset based on the `bases` and determine which is most appropriate. This implies some order of precedence in these locations and some file-system operation to determine if the asset there. - This is the `defaultJoinOperation`. +The default implementation is suitable for most users but can be customised per the `join` option. -When using `createJoinFunction` it's common to customise one of the `generator` or `operation` parameters and leave the other as default. +A custom `join` function from scratch is possible but we've provided some [building blocks](#building-blocks) to make the task easier. -## Reference +## Building blocks -For full reference check the source code in [`lib/join-function/index.js`](../lib/join-function/index.js). +There are number of utilities (defined in [`lib/join-function/index.js`](../lib/join-function/index.js)) to help construct a custom "join" function . These are conveniently re-exported as properties of the loader. -The default "join" function is exported as `defaultJoin` and is equivalent to the following. +These utilities are used to create the `defaultJoin` as follows. ```javascript const { createJoinFunction, + createJoinImplementation, defaultJoinGenerator, - defaultJoinOperation, - defaultJoin } = require('resolve-url-loader'); // create a join function equivalent to "defaultJoin" -const myJoinFn = createJoinFunction({ - name : 'myJoinFn', - scheme : 'alstroemeria', - generator: defaultJoinGenerator, - operation: defaultJoinOperation +const myJoinFn = createJoinFunction( + 'myJoinFn', + createJoinImplementation(defaultJoinGenerator), }); ``` -The `name: string` is used purely for debugging purposes. +🤓 If you have some very specific behaviour in mind you can specify your own implementation. This gives full control but still gives you `debug` logging for free. -The `scheme: string` should be a literal string of the current scheme and should match the value shown in the loader `package.json` at the time you first author your custom join function. +```javascript +createJoinFunction = (name:string, implementation: function): function +``` + +For each item, the implementation needs to make multiple attempts at locating the asset. It has mixed concerns of itentifying locations to search and then evaluating those locates one by one. -The `generator: function` chooses the order of potential base paths to consider. +👉 However its recommended to instead use `createJoinImplementation` to create the `implementation` using the `generator` concept. ```javascript -generator (filename: string, uri: string, bases: {}, isAbsolute:boolean, options: {}) => Array | Iterable +createJoinImplementation = (generator: function*): function ``` -* The `filename: string` is the loader `resourcePath`. -* The `uri: string` is the argument to the `url()` as it appears in the source file. -* The `bases: {}` are a hash where the keys are the sourcemap evaluation locations in the [algorithm](./how-it-works.md#algorithm) and the values are absolute paths that the sourcemap reports. These directories might not actually exist. -* The `isAbsolute: boolean` flag indicates whether the URI is considered an absolute file or root relative path by webpack's definition. Absolute URIs are only processed if the `root` option is specified. -* The `options: {}` are the loader options as configured in webpack. This includes documented options as well as any you add in your configuration. +The `generator` has the single concern of identifying locations to search. The work of searching these locations is done by `createJoinImplementation`. Overall this means less boilerplate code for you to write. + +Don't worry, you don't need to use `function*` semantics for the `generator` unless you want to. + +## Simple customisation + +It is relatively simple to change the precedence of values (from the [algorithm](./how-it-works.md#algorithm)) or add further locations to search for an asset. To do this we use `createJoinImplementation` and write a custom `generator`. -The `operation: function` is the predicate which determines whether a base path is successful. Each may be modified or the default ones used as shown above. +See the reference or jump directly to the [examples](#how-to-change-precedence-of-source-locations). +### Reference + +The `generator` identifies `[base:string,uri:string]` tuples describing locations to search for an asset. It does **not** return the final asset path. + +You may lazily generate tuples as `Iterator`. Refer to this [guide on Iterators and Generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators). ```javascript -operation (filename: string, uri: string, base: string, next: (string?) => string, options: {}) => string +generator = function* (item: {}, options: {}, loader: {}): Iterator<[string,string]> ``` -* The `filename: string` is the loader `resourcePath`. -* The `uri: string` is the argument to the `url()` as it appears in the source file. -* The `base: string` is a single base path where the URI may exist. This directory might not actually exist. -* The `next: (string?) => string` function is only called on failure. Passing any value to `next(...)` marks that as a fallback value but only the earliest fallback is ever used. Always `return` the result of calling `next()`. -* The `options: {}` are the loader options as configured in webpack. This includes documented options as well as any you add in your configuration. +Or it can be simpler to write a function that returns `Array` and convert it to a generator using `asGenerator`. -## How to +```javascript +generator = asGenerator( function (item: {}, options: {}, loader: {}): Array ) +``` +```javascript +generator = asGenerator( function (item: {}, options: {}, loader: {}): Array<[string,string]> ) +``` -### How to: sample source-map differently +When using `asGenerator` you may return elements as either `base:string` **or** `[base:string,uri:string]` tuples. -Source-map sampling is limited to the locations defined in the [algoritm](./how-it-works.md#algorithm). +
+Arguments -However you can preference these locations in a different order using a custom `generator`. You can even make the order dependent on the `filename` or `uri` being processed or some additional `options` you add to the loader. +* `item` consist of - + * `uri: string` is the argument to the `url()` as it appears in the source file. + * `query: string` is any query or hash string starting with `?` or `#` that suffixes the `uri` + * `isAbsolute: boolean` flag indicates whether the URI is considered an absolute file or root relative path by webpack's definition. Absolute URIs are only processed if the `root` option is specified. + * `bases: {}` are a hash where the keys are the sourcemap evaluation locations in the [algorithm](./how-it-works.md#algorithm) and the values are absolute paths that the sourcemap reports. These directories might not actually exist. +* `options` consist of - + * All documented options for the loader. + * Any other values you include in the loader configuration for your own purposes. +* `loader` consists of the webpack loader API, useful items include - + * `fs: {}` the virtual file-system from Webpack. + * `resourcePath: string` the source file currently being processed. +* returns an `Iterator` with elements of `[base:string,uri:string]` either intrinsically or by using `asGenerator`. +
-Absolute URIs are rare in most projects but can be handled for completeness. +
+FAQ + +* **Why a tuple?** + + The primary pupose of this loader is to find the correct `base` path for your `uri`. By returning a list of paths to search we can better generate `debug` logging. + + That said there are cases where you might want to amend the `uri`. The solution is to make each element a tuple of `base` and `uri` representing a potential location to find the asset. + + If you'e interested only in the `base` path and don't intend to vary the `uri` then the `asGenerator` utility saves you having to create repetiative tuples (and from using `function*` semantics). + +* **Can I vary the `query` using the tuple?** + + No. We don't support amending the `query` in the final value. If you would like this enhancement please open an issue. + +* **What about duplicate or falsey elements?** + + The `createJoinImplementation` will eliminate any invalid elements regardless of whether you use `Array` or `Iterator`. This makes it possible to `&&` elements inline with a predicate value. + + If you use `Array` then `asGenerator` will also remove duplicates. + +* **When should I use `function*`?** + + If you need lazy generation of values then you may return `Iterator` or use `function*` semantics. Refer to [this guide on Iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators). + + But in most cases, when the values are known apriori, simply returning `Array` has simpler semantics making `asGenerator` preferable. + +* **Why is this generator so complicated?** + + The join function must make multiple attempts to join a `base` and `uri` and check that the file exists using webpack `fs`. + + The `generator` is focussed on identifying locations to search. It is a more scalable concept where you wish to search many places. The traditional use case for the custom "join" function is a file-system search so the `generator` was designed to make this possible. + + If you prefer a less abstract approach consider a full `implementation` per the [full customisation](#full-customisation) approach. +
+ +### How to: change precedence of source locations + +Source-map sampling is limited to the locations defined in the [algorithm](./how-it-works.md#algorithm). You can't change these locations but you can preference them in a different order. + +This example shows the default order which you can easily amend. Absolute URIs are rare in most projects but can be handled for completeness. + +**Using `asGenerator`** ```javascript const { createJoinFunction, - defaultJoinOperation, + createJoinImplementation, + asGenerator, + defaultJoinGenerator, } = require('resolve-url-loader'); -// order source-map sampling location by your preferred priority -const myGenerator = (filename, uri, bases, isAbsolute, options) => - isAbsolute ? [options.root] : [bases.subString, bases.value, bases.property, bases.selector]; - -const myJoinFn = createJoinFunction({ - name : 'myJoinFn', - scheme : 'alstroemeria', - generator: myGenerator, - operation: defaultJoinOperation -}); +// order source-map sampling location by your preferred precedence (matches defaultJoinGenerator) +const myGenerator = asGenerator( + ({ isAbsolute, bases: { substring, value, property, selector} }, { root }) => + isAbsolute ? [root] : [subString, value, property, selector] +); + +const myJoinFn = createJoinFunction( + 'myJoinFn', + createJoinImplementation(myGenerator), +); ``` -Notes +**Notes** + +* The implementation is the default behaviour, so if you want this precedence do **not** customise the `join` option. +* Absolute URIs generally use the base path given in the `root` option as shown. +* The `asGenerator` utility allows us to return simple `Array` of potential base paths. -* It's usually clearer for a simple `generator` to return `Array`. Reserve using `function*` for where lazy evaluation is important. +### How to: fallback to a theme or other global directory -### How to: resolve to an asset that may not exist +Additional locations can be added by decorating the default generator. This is popular for adding some sort of "theme" directory containing assets. -The default `operation` determines success based on whether a file exists in the webpack file system. If you wish to resolve a different file at the same location you will need to customise the `operation`. +This example appends a static theme directory as a fallback location where the asset might reside. Absolute URIs are rare in most projects but can be handled for completeness. -**example:** resolve a `.zip` file if the `.png` doesn't exist. +**Using `asGenerator`** ```javascript +const path = require('path'); const { createJoinFunction, + createJoinImplementation, + asGenerator, defaultJoinGenerator, - webpackExistsSync } = require('resolve-url-loader'); -// accept a ".zip" file if the ".png" doesn't exist. -const myOperation = (filename, uri, base, next, options) => { - const absolute = path.normalize(path.join(base, uri)); - const absoluteZip = absolute + '.zip'; - return ( - webpackExistsSync(options.fs, absolute) && options.fs.statSync(absolute).isFile() && absolute || - webpackExistsSync(options.fs, absoluteZip) && options.fs.statSync(absoluteZip).isFile() && absoluteZip || - next(absolute) - ); -}; +const myThemeDirectory = path.resolve(...); -const myJoinFn = createJoinFunction({ - name : 'myJoinFn', - scheme : 'alstroemeria', - generator: defaultJoinGenerator, - operation: myOperation -}); +// call default generator then append any additional paths +const myGenerator = asGenerator( + (item, ...rest) => [ + ...defaultJoinGenerator(item, ...rest), + item.isAbsolute ? null : myThemeDirectory, + ] +); + +const myJoinFn = createJoinFunction( + 'myJoinFn', + createJoinImplementation(myGenerator), +); ``` -Notes +**Notes** + +* By spreading the result of `defaultJoinGenerator` we are first trying the default behaviour. If that is unsuccessful we then try the theme location. +* It's assumed that theming doesn't apply to absolute URIs. Since falsey elements are ignored we can easily `null` the additional theme element inline as shown. +* The `asGenerator` utility allows us to return simple `Array` of potential base paths. + +### How to: fallback to some other asset file + +Lets imagine we don't have high quality files for all our assets and must sometimes use a lower quality format. For each item we need to try the `uri` with different file extensions. We can do this by returning tuples of `[base:string,uri:string]`. + +In this example we prefer the `.svg` asset we are happy to use any available `.png` or `.jpg` instead. + +**Using `asGenerator`** + +```javascript +const { + createJoinFunction, + createJoinImplementation, + asGenerator, + defaultJoinGenerator, +} = require('resolve-url-loader'); -* You may combine `base` and `uri` arbitrarily since you control the "join". +// call default generator then pair different variations of uri with each base +const myGenerator = asGenerator( + (item, ...rest) => { + const defaultTuples = [...defaultJoinGenerator(item, ...rest)]; + return /\.svg$/.test(item.uri) + ? ['.svg', '.png', 'jpg'].flatMap((ext) => + defaultTuples.flatMap(([base, uri]) => + [base, uri.replace(/\.svg$/, ext)] + }) + ) + : defaultTuples; + } +); -* You determine the predicate for success, but if the returned file does not exist then webpack will fail. +const myJoinFn = createJoinFunction( + 'myJoinFn', + createJoinImplementation(myGenerator), +); +``` -* The webpack file-system is provided by the `enhanced-resolver-plugin` and does not contain `fs.existsSync`. Instead, use the `webpackExistsSync` utility function as shown. +**Using `function*`** -* Always `return` the result of calling `next()`. +```javascript +const { + createJoinFunction, + createJoinImplementation, + defaultJoinGenerator, +} = require('resolve-url-loader'); + +// call default generator then pair different variations of uri with each base +const myGenerator = function* (item, ...rest) { + if (/\.svg$/.test(item.uri)) { + for (let ext of ['.svg', '.png', 'jpg']) { + for (let [base, uri] of defaultJoinGenerator(item, ...rest)) { + yield [base, uri.replace(/\.svg$/, ext)]; + } + } + } else { + for (let value of defaultJoinGenerator(item, ...rest)) { + yield value; + } + } +} + +const myJoinFn = createJoinFunction( + 'myJoinFn', + createJoinImplementation(myGenerator), +); +``` + +**Notes** + +* Existing generators such as `defaultJoinGenerator` will always return `[string,string]` tuples so we can destruture `base` and `uri` values with confidence. +* This implementation attempts all extensions for a given `base` before moving to the next `base`. Obviously we may change the nesting and instead do the oposite, attempt all bases for a single extension before moving on to the next extension +* The `asGenerator` utility allows us to return `Array<[string, string]>` but is **not** needed when we use `function*` semantics. ### How to: perform a file-system search for an asset ⚠️ **IMPORTANT** - This example is indicative only and is **not** advised. -When this loader was originally released it was very common for packages be broken to the point that a full file search was needed to locate assets referred to in CSS. While this was not performant some users really liked it. +When this loader was originally released it was very common for packages be broken to the point that a full file search was needed to locate assets referred to in CSS. While this was not performant some users really liked it. By customising the `generator` we can once again lazily search the file-system. -By customising the `generator` it is possibly to lazily provide a file search. So long as your criteria for success is that the file exists then the default `operation` can by used. +In this example we search the parent directories of the base paths, continuing upwards until we hit a package boundary. Absolute URIs are rare in most projects but can be handled for completeness. -**example:** search parent directories of the initial base path until you hit a package boundary +**Using `function*`** ```javascript +const path = require('path'); const { createJoinFunction, - defaultJoinOperation, + createJoinImplementation, webpackExistsSync } = require('resolve-url-loader'); // search up from the initial base path until you hit a package boundary -const myGenerator = function* (filename, uri, bases, isAbsolute, options) { +const myGenerator = function* ( + { uri, isAbsolute, bases: { substring, value, property, selector } }, + { root, attempts = 1e3 }, + { fs }, +) { if (isAbsolute) { - yield options.root; + yield [root, uri]; } else { - for (let base of [bases.subString, bases.value, bases.property, bases.selector]) { - let isDone = false; - for (let isDone = false, attempts = 1e3; !isDone && attempts > 0; attempts--) { + for (let base of [subString, value, property, selector]) { + for (let isDone = false, i = 0; !isDone && i < attempts; i++) { + yield [base, uri]; + // unfortunately fs.existsSync() is not present so we must shim it const maybePkg = path.normalize(path.join(base, 'package.json')); - isDone = webpackExistsSync(options.fs, maybePkg) && options.fs.statSync(maybePkg).isFile(); - yield base; + try { + isDone = fs.statSync(maybePkg).isFile(); + } catch (error) { + isDone = false; + } base = base.split(/(\\\/)/).slice(0, -2).join(''); } } } -} +}; -const myJoinFn = createJoinFunction({ - name : 'myJoinFn', - scheme : 'alstroemeria', - generator: myGenerator, - operation: defaultJoinOperation -}); +const myJoinFn = createJoinFunction( + 'myJoinFn', + createJoinImplementation(myGenerator), +); ``` -Notes +**Notes** * This implementation is nether tested nor robust, it would need further safeguards to avoid searching the entire file system. * By using `function*` the generator is lazy. We only walk the file-system directory tree as necessary. -* The webpack file-system is provided by the `enhanced-resolver-plugin` and does not contain `fs.existsSync`. Instead, use the `webpackExistsSync` utility function as shown. +* The webpack file-system is provided by the `enhanced-resolver-plugin` and does **not** contain `fs.existsSync()`. We must use `fs.statsSync()` instead and catch any error where the file isn't present. + +* You may set additional `options` when you configure the loader in webpack and then access them in your `generator`. In this case we add an `attempts` option to limit the file search. + + +## Full customisation + +The `createJoinFunction` can give you full control over how the `base` and `uri` are joined to create an absolute file path **and** the definitiion of success for that combination. + +It provides additional logging when using `debug` option so is a better choice then writing a "join" function from scratch. -* You may set additional `options` when you configure the loader in webpack that you can then access in the generator. In this case the `attempts` could be made a configurable option. +Limited documentation is given here since it is rare to require a full customisation. Refer to the source code for further information. + +### Reference + +The `implementation` synchronously returns the final asset path or some fallback value. It makes a number of attempts to search for the given item and returns an element describing each attempt. + +```javascript +implementation = function (item: {}, options: {}, loader: {}): + Array<{ + base : string, + uri : string, + joined : string, + isSuccess : boolean, + isFallback: boolean, + }> +``` +
+Arguments + +* `item` consist of - + * `uri: string` is the argument to the `url()` as it appears in the source file. + * `query: string` is any string starting with `?` or `#` that suffixes the `uri` + * `isAbsolute: boolean` flag indicates whether the URI is considered an absolute file or root relative path by webpack's definition. Absolute URIs are only processed if the `root` option is specified. + * `bases: {}` are a hash where the keys are the sourcemap evaluation locations in the [algorithm](./how-it-works.md#algorithm) and the values are absolute paths that the sourcemap reports. These directories might not actually exist. +* `options` consist of - + * All documented options for the loader. + * Any other values you include in the loader configuration for your own purposes. +* `loader` consists of the webpack loader API, useful items include - + * `fs: {}` the virtual file-system from Webpack. + * `resourcePath: string` the source file currently being processed. +* returns an array of attempts that were made in resolving the URI - + * `base` the base path + * `uri` the uri path + * `joined` the absolute path created from the joining the `base` and `uri` paths. + * `isSuccess` indicates the asset was found and that `joined` should be the final result + * `isFallback` indicates the asset was not found but that `joined` kis suitable as a fallback value +
\ No newline at end of file diff --git a/packages/resolve-url-loader/index.js b/packages/resolve-url-loader/index.js index 7c04963..13c2713 100644 --- a/packages/resolve-url-loader/index.js +++ b/packages/resolve-url-loader/index.js @@ -12,9 +12,9 @@ var path = require('path'), var adjustSourceMap = require('adjust-sourcemap-loader/lib/process'); -var valueProcessor = require('./lib/value-processor'); -var joinFn = require('./lib/join-function'); -var logToTestHarness = require('./lib/log-to-test-harness'); +var valueProcessor = require('./lib/value-processor'), + joinFn = require('./lib/join-function'), + logToTestHarness = require('./lib/log-to-test-harness'); const DEPRECATED_OPTIONS = { engine: [ @@ -74,7 +74,6 @@ function resolveUrlLoader(content, sourceMap) { var rawOptions = loaderUtils.getOptions(loader), options = Object.assign( { - fs : loader.fs, sourceMap: loader.sourceMap, engine : 'postcss', silent : false, @@ -106,10 +105,24 @@ function resolveUrlLoader(content, sourceMap) { 'loader misconfiguration', '"join" option must be a Function' ); - } else if (options.join.length !== 1) { + } else if (options.join.length !== 2) { return handleAsError( 'loader misconfiguration', - '"join" Function must take exactly 1 arguments (options hash)' + '"join" Function must take exactly 2 arguments (options, loader)' + ); + } + + // validate the result of calling the join option + var joinProper = options.join(options, loader); + if (typeof joinProper !== 'function') { + return handleAsError( + 'loader misconfiguration', + '"join" option must itself return a Function when it is called' + ); + } else if (joinProper.length !== 1) { + return handleAsError( + 'loader misconfiguration', + '"join" Function must create a function that takes exactly 1 arguments (item)' ); } @@ -198,10 +211,14 @@ function resolveUrlLoader(content, sourceMap) { Promise .resolve(engine(loader.resourcePath, content, { outputSourceMap : !!options.sourceMap, - transformDeclaration: valueProcessor(loader.resourcePath, options), absSourceMap : absSourceMap, sourceMapConsumer : sourceMapConsumer, - removeCR : options.removeCR + removeCR : options.removeCR, + transformDeclaration: valueProcessor({ + join : joinProper, + root : options.root, + directory: path.dirname(loader.resourcePath) + }) })) .catch(onFailure) .then(onSuccess); diff --git a/packages/resolve-url-loader/lib/engine/rework.js b/packages/resolve-url-loader/lib/engine/rework.js index 38359b6..c76ad69 100644 --- a/packages/resolve-url-loader/lib/engine/rework.js +++ b/packages/resolve-url-loader/lib/engine/rework.js @@ -146,7 +146,7 @@ function requireOptionalPeerDependency(moduleName) { } catch (error) { if (error.message === 'Cannot find module \'' + moduleName + '\'') { - throw new Error('To use the "rework" engine you must install the optionalPeerDependencies'); + throw new Error('to use the "rework" engine you must install the optionalPeerDependencies'); } else { throw error; diff --git a/packages/resolve-url-loader/lib/join-function/debug.js b/packages/resolve-url-loader/lib/join-function/debug.js index 82e7ebf..7224f3d 100644 --- a/packages/resolve-url-loader/lib/join-function/debug.js +++ b/packages/resolve-url-loader/lib/join-function/debug.js @@ -4,43 +4,56 @@ */ 'use strict'; -var path = require('path'); +const path = require('path'); -var PACKAGE_NAME = require('../../package.json').name; +const PACKAGE_NAME = require('../../package.json').name; + +/** + * Paths are formatted to have posix style path separators and those within the CWD are made relative to CWD. + * + * @param {string} absolutePath An absolute path to format + * @returns {string} the formatted path + */ +const pathToString = (absolutePath) => { + if (absolutePath === '') { + return '-empty-'; + } else { + const relative = path.relative(process.cwd(), absolutePath).split(path.sep); + const segments = + (relative[0] !== '..') ? ['.'].concat(relative).filter(Boolean) : + (relative.lastIndexOf('..') < 2) ? relative : + absolutePath.split(path.sep); + return segments.join('/'); + } +}; + +exports.pathToString = pathToString; /** * Format a debug message. * - * @param {string} file The file being processed by webpack + * @param {string} filename The file being processed by webpack * @param {string} uri A uri path, relative or absolute - * @param {Array} bases Absolute base paths up to and including the found one - * @param {boolean} isFound Indicates the last base was a positive match + * @param {Array<{base:string,joined:string,isSuccess:boolean}>} attempts An array of attempts, possibly empty * @return {string} Formatted message */ -function formatJoinMessage(file, uri, bases, isFound) { - return [PACKAGE_NAME + ': ' + pathToString(file) + ': ' + uri] - .concat(bases.map(pathToString)) - .concat(isFound ? 'FOUND' : 'NOT FOUND') - .join('\n '); +const formatJoinMessage = (filename, uri, attempts) => { + const attemptToCells = (_, i, array) => { + const { base: prev } = (i === 0) ? {} : array[i-1]; + const { base: curr, joined } = array[i]; + return [(curr === prev) ? '' : pathToString(curr), pathToString(joined)]; + }; - /** - * If given path is within `process.cwd()` then show relative posix path, otherwise show absolute posix path. - * - * @param {string} absolute An absolute path - * @return {string} A relative or absolute path - */ - function pathToString(absolute) { - if (!absolute) { - return '-empty-'; - } else { - var relative = path.relative(process.cwd(), absolute) - .split(path.sep); + const formatCells = (lines) => { + const maxWidth = lines.reduce((max, [cellA]) => Math.max(max, cellA.length), 0); + return lines.map(([cellA, cellB]) => [cellA.padEnd(maxWidth), cellB]).map((cells) => cells.join(' --> ')); + }; - return ((relative[0] === '..') ? absolute.split(path.sep) : ['.'].concat(relative).filter(Boolean)) - .join('/'); - } - } -} + return [PACKAGE_NAME + ': ' + pathToString(filename) + ': ' + uri] + .concat(attempts.length === 0 ? '-empty-' : formatCells(attempts.map(attemptToCells))) + .concat(attempts.some(({ isSuccess }) => isSuccess) ? 'FOUND' : 'NOT FOUND') + .join('\n '); +}; exports.formatJoinMessage = formatJoinMessage; @@ -56,20 +69,18 @@ exports.formatJoinMessage = formatJoinMessage; * @param {function|boolean} debug A boolean or debug function * @return {function(function, array):void} A logging function possibly degenerate */ -function createDebugLogger(debug) { - var log = !!debug && ((typeof debug === 'function') ? debug : console.log), - cache = {}; - return log ? actuallyLog : noop; - - function noop() {} - - function actuallyLog(msgFn, params) { - var key = Function.prototype.toString.call(msgFn) + JSON.stringify(params); - if (!cache[key]) { - cache[key] = true; - log(msgFn.apply(null, params)); - } - } -} +const createDebugLogger = (debug) => { + const log = !!debug && ((typeof debug === 'function') ? debug : console.log); + const cache = {}; + return log ? + ((msgFn, params) => { + const key = Function.prototype.toString.call(msgFn) + JSON.stringify(params); + if (!cache[key]) { + cache[key] = true; + log(msgFn.apply(null, params)); + } + }) : + (() => undefined); +}; exports.createDebugLogger = createDebugLogger; diff --git a/packages/resolve-url-loader/lib/join-function/debug.test.js b/packages/resolve-url-loader/lib/join-function/debug.test.js new file mode 100644 index 0000000..07067e0 --- /dev/null +++ b/packages/resolve-url-loader/lib/join-function/debug.test.js @@ -0,0 +1,242 @@ +/* + * MIT License http://opensource.org/licenses/MIT + * Author: Ben Holloway @bholloway + */ +'use strict'; + +const {resolve} = require('path'); +const tape = require('blue-tape'); +const sinon = require('sinon'); +const outdent = require('outdent'); + +const { createDebugLogger, formatJoinMessage } = require('./debug'); + +const json = (strings, ...substitutions) => + String.raw( + strings, + ...substitutions.map(v => JSON.stringify(v, (_, vv) => Number.isNaN(vv) ? 'NaN' : vv)) + ); + +tape( + 'debug', + ({name, test, end: end1, equal, looseEqual}) => { + test(`${name} / formatJoinMessage()`, ({end: end2}) => { + [ + // absolute within cwd + [ + [ + resolve('my-source-file.js'), + 'my-asset.png', + [ + { + base: resolve('foo'), + joined: resolve('foo', 'my-asset.png'), + isSuccess: false + }, { + base: resolve('bar', 'baz'), + joined: resolve('bar', 'baz', 'my-asset.png'), + isSuccess: true + } + ] + ], + outdent` + resolve-url-loader: ./my-source-file.js: my-asset.png + ./foo --> ./foo/my-asset.png + ./bar/baz --> ./bar/baz/my-asset.png + FOUND + ` + ], + // absolute otherwise + [ + [ + '/my-source-file.js', + '#anything\\./goes', + [ + { + base: '/foo', + joined: '/foo/#anything\\./goes', + isSuccess: false + }, { + base: '/bar/baz', + joined: '/bar/baz/#anything\\./goes', + isSuccess: false + } + ] + ], + outdent` + resolve-url-loader: /my-source-file.js: #anything\\./goes + /foo --> /foo/#anything\\./goes + /bar/baz --> /bar/baz/#anything\\./goes + NOT FOUND + ` + ], + // presumed relative + [ + [ + 'my-source-file.js', + 'my-asset.png', + [ + { + base: 'foo', + joined: 'foo/my-asset.png', + isSuccess: true, + }, { + base: 'bar/baz', + joined: 'bar/baz/my-asset.png', + isSuccess: false + } + ] + ], + outdent` + resolve-url-loader: ./my-source-file.js: my-asset.png + ./foo --> ./foo/my-asset.png + ./bar/baz --> ./bar/baz/my-asset.png + FOUND + ` + ], + // explicitly relative + [ + [ + './my-source-file.js', + 'my-asset.png', + [ + { + base: './foo', + joined: './foo/my-asset.png', + isSuccess: false, + }, { + base: './bar/baz', + joined: './bar/baz/my-asset.png', + isSuccess: true, + } + ] + ], + outdent` + resolve-url-loader: ./my-source-file.js: my-asset.png + ./foo --> ./foo/my-asset.png + ./bar/baz --> ./bar/baz/my-asset.png + FOUND + ` + ], + [ + [ + '../my-source-file.js', + 'my-asset.png', + [ + { + base: '../foo', + joined: '../foo/my-asset.png', + isSuccess: false + }, { + base: '../bar/baz', + joined: '../bar/baz/my-asset.png', + isSuccess: false + } + ] + ], + outdent` + resolve-url-loader: ../my-source-file.js: my-asset.png + ../foo --> ../foo/my-asset.png + ../bar/baz --> ../bar/baz/my-asset.png + NOT FOUND + ` + ], + // empty + [ + ['./my-source-file.js', 'my-asset.png', []], + outdent` + resolve-url-loader: ./my-source-file.js: my-asset.png + -empty- + NOT FOUND + ` + ], + ].forEach(([input, expected]) => + equal( + formatJoinMessage(...input), + expected, + json`input ${input} should sanitise to ${expected}` + ) + ); + + end2(); + }); + + test(`${name} / createDebugLogger()`, ({name: name2, test: test2, end: end2}) => { + test2(`${name2} / false`, ({end: end3}) => { + const sandbox = sinon.createSandbox(); + const factory = sandbox.fake.returns('foo'); + const consoleLog = sandbox.stub(console, 'log'); + + const logger = createDebugLogger(false); + logger(factory); + sandbox.restore(); + + equal(consoleLog.callCount, 0, 'should not call console.log()'); + equal(factory.callCount, 0, 'should not call underlying message factory'); + + end3(); + }); + + test2(`${name2} / true`, ({end: end3}) => { + const sandbox = sinon.createSandbox(); + const factory = sandbox.fake.returns('foo'); + const consoleLog = sandbox.stub(console, 'log'); + + const logger = createDebugLogger(true); + logger(factory, ['bar', 1, false]); + logger(factory, ['baz', 2, undefined]); + sandbox.restore(); + + equal(factory.callCount, 2, 'should call underlying log message factory'); + looseEqual( + factory.args, + [['bar', 1, false], ['baz', 2, undefined]], + 'should call underlying message factory with expected arguments' + ); + + equal(consoleLog.callCount, 2, 'should call console.log()'); + looseEqual( + consoleLog.args, + [['foo'], ['foo']], + 'should log expected value' + ); + + end3(); + }); + + test2(`${name2} / logFn`, ({end: end3}) => { + const sandbox = sinon.createSandbox(); + const factory = sandbox.fake.returns('foo'); + const consoleLog = sandbox.stub(console, 'log'); + const logFn = sandbox.spy(); + + const logger = createDebugLogger(logFn); + logger(factory, ['bar', 1, false]); + logger(factory, ['baz', 2, undefined]); + sandbox.restore(); + + equal(factory.callCount, 2, 'should call underlying log message factory'); + looseEqual( + factory.args, + [['bar', 1, false], ['baz', 2, undefined]], + 'should call underlying message factory with expected arguments' + ); + + equal(logFn.callCount, 2, 'should call logFn()'); + looseEqual( + logFn.args, + [['foo'], ['foo']], + 'should log expected value' + ); + + equal(consoleLog.callCount, 0, 'should not call console.log()'); + + end3(); + }); + + end2(); + }); + + end1(); + } +); diff --git a/packages/resolve-url-loader/lib/join-function/fs-utils.js b/packages/resolve-url-loader/lib/join-function/fs-utils.js new file mode 100644 index 0000000..303b7a1 --- /dev/null +++ b/packages/resolve-url-loader/lib/join-function/fs-utils.js @@ -0,0 +1,24 @@ +/* + * MIT License http://opensource.org/licenses/MIT + * Author: Ben Holloway @bholloway + */ +'use strict'; + +const fsUtils = (fs) => { + // fs from enhanced-resolver doesn't include fs.existsSync so we need to use fs.statsSync instead + const withStats = (fn) => (absolutePath) => { + try { + return fn(fs.statSync(absolutePath)); + } catch (e) { + return false; + } + }; + + return { + isFileSync: withStats((stats) => stats.isFile()), + isDirectorySync: withStats((stats) => stats.isDirectory()), + existsSync: withStats((stats) => stats.isFile() || stats.isDirectory()) + }; +}; + +module.exports = fsUtils; \ No newline at end of file diff --git a/packages/resolve-url-loader/lib/join-function/index.js b/packages/resolve-url-loader/lib/join-function/index.js index d7e49f2..bd0be61 100644 --- a/packages/resolve-url-loader/lib/join-function/index.js +++ b/packages/resolve-url-loader/lib/join-function/index.js @@ -4,208 +4,232 @@ */ 'use strict'; -var path = require('path'); +const path = require('path'); -var sanitiseIterable = require('./sanitise-iterable'), - debug = require('./debug'); +const { createDebugLogger, formatJoinMessage } = require('./debug'); +const fsUtils = require('./fs-utils'); -/** - * Generated name from "flower" series - * @see https://gf.dev/sprintname - */ -var CURRENT_SCHEME = require('../../package.json').scheme; +const ITERATION_SAFETY_LIMIT = 100e3; /** - * Webpack `fs` from `enhanced-resolve` doesn't support `existsSync()` so we shim using `statsSync()`. + * Wrap a function such that it always returns a generator of tuple elements. * - * @param {{statSync:function(string):{isFile:function():boolean}}} webpackFs The webpack `fs` from `loader.fs`. - * @param {string} absolutePath Absolute path to the file in question - * @returns {boolean} True where file exists, else False + * @param {function({uri:string},...):(Array|Iterator)<[string,string]|string>} fn The function to wrap + * @returns {function({uri:string},...):(Array|Iterator)<[string,string]>} A function that always returns tuple elements */ -function webpackExistsSync(webpackFs, absolutePath) { - try { - return webpackFs.statSync(absolutePath).isFile(); - } catch (e) { - return false; - } -} - -exports.webpackExistsSync = webpackExistsSync; +const asGenerator = (fn) => { + const toTuple = (defaults) => (value) => { + const partial = [].concat(value); + return [...partial, ...defaults.slice(partial.length)]; + }; + + const isTupleUnique = (v, i, a) => { + const required = v.join(','); + return a.findIndex((vv) => vv.join(',') === required) === i; + }; + + return (item, ...rest) => { + const {uri} = item; + const mapTuple = toTuple([null, uri]); + const pending = fn(item, ...rest); + if (Array.isArray(pending)) { + return pending.map(mapTuple).filter(isTupleUnique)[Symbol.iterator](); + } else if ( + pending && + (typeof pending === 'object') && + (typeof pending.next === 'function') && + (pending.next.length === 0) + ) { + return pending; + } else { + throw new TypeError(`in "join" function expected "generator" to return Array|Iterator`); + } + }; +}; + +exports.asGenerator = asGenerator; /** - * The default iterable factory will order `subString` then `value` then `property` then `selector`. + * A high-level utility to create a join function. * - * @param {string} filename The absolute path of the file being processed - * @param {string} uri The uri given in the file webpack is processing - * @param {boolean} isAbsolute True for absolute URIs, false for relative URIs - * @param {{subString:string, value:string, property:string, selector:string}} bases A hash of possible base paths - * @param {{fs:Object, root:string, debug:boolean|function}} options The loader options including webpack file system - * @returns {Array} An iterable of possible base paths in preference order - */ -function defaultJoinGenerator(filename, uri, isAbsolute, bases, options) { - return isAbsolute ? [options.root] : [bases.subString, bases.value, bases.property, bases.selector]; -} - -exports.defaultJoinGenerator = defaultJoinGenerator; - -/** - * The default operation simply joins the given base to the uri and returns it where it exists. - * - * The result of `next()` represents the eventual result and needs to be returned otherwise. + * The `generator` is responsible for ordering possible base paths. The `operation` is responsible for joining a single + * `base` path with the given `uri`. The `predicate` is responsible for reporting whether the single joined value is + * successful as the overall result. * - * If none of the expected files exist then any given `fallback` argument to `next()` is used even if it does not exist. + * Both the `generator` and `operation` may be `function*()` or simply `function(...):Array`. * - * @param {string} filename The absolute path of the file being processed - * @param {string} uri The uri given in the file webpack is processing - * @param {string} base A value from the iterator currently being processed - * @param {function(?string):>} next Optionally set fallback then recurse next iteration - * @param {{fs:Object, root:string, debug:boolean|function}} options The loader options including webpack file system - * @returns {string|Array} Result from the last iteration that occurred - */ -function defaultJoinOperation(filename, uri, base, next, options) { - var absolute = path.normalize(path.join(base, uri)), - isSuccess = webpackExistsSync(options.fs, absolute) && options.fs.statSync(absolute).isFile(); - return isSuccess ? absolute : next(absolute); -} - -exports.defaultJoinOperation = defaultJoinOperation; - -/** - * The default join function iterates over possible base paths until a suitable join is found. - * - * The first base path is used as fallback for the case where none of the base paths can locate the actual file. - * - * @type {function} - */ -exports.defaultJoin = createJoinFunction({ - name : 'defaultJoin', - scheme : CURRENT_SCHEME, - generator: defaultJoinGenerator, - operation: defaultJoinOperation -}); - -/** - * A utility to create a join function. - * - * Refer to implementation of `defaultJoinGenerator` and `defaultJoinOperation`. - * - * @param {string} name Name for the resulting join function - * @param {string} scheme A keyword that confirms your implementation matches the current scheme. - * @param {function(string, {subString:string, value:string, property:string, selector:string}, Object): + * @param {function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string, + * selector:string}}, {filename:string, fs:Object, debug:function|boolean, root:string}): * (Array|Iterator)} generator A function that takes the hash of base paths from the `engine` and * returns ordered iterable of paths to consider - * @param {function({filename:string, uri:string, base:string}, function(?string):>, - * {fs:Object, root:string}):(string|Array)} operation A function that tests values and returns joined paths - * @returns {function(string, {fs:Object, debug:function|boolean, root:string}): - * (function(string, {subString:string, value:string, property:string, selector:string}):string)} join function factory + * @returns {function({filename:string, fs:Object, debug:function|boolean, root:string}): + * (function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string, + * selector:string}}):string)} join implementation */ -function createJoinFunction({ name, scheme, generator, operation }) { - if (typeof scheme !== 'string' || scheme.toLowerCase() !== CURRENT_SCHEME) { - throw new Error(`Custom join function has changed, please update to the latest scheme. Refer to the docs.`); +const createJoinImplementation = (generator) => (item, options, loader) => { + const { isAbsolute } = item; + const { root } = options; + const { fs } = loader; + + // generate the iterator + const iterator = generator(item, options, loader); + const isValidIterator = iterator && typeof iterator === 'object' && typeof iterator.next === 'function'; + if (!isValidIterator) { + throw new Error('expected generator to return Iterator'); } - /** - * A factory for a join function with logging. - * - * Options are curried and a join function proper is returned. - * - * @param {{fs:Object, root:string, debug:boolean|function}} options The loader options including webpack file system - */ - function join(options) { - var log = debug.createDebugLogger(options.debug); - - /** - * Join function proper. - * - * For absolute uri only `uri` will be provided and no `bases`. - * - * @param {string} filename The current file being processed - * @param {string} uri A uri path, relative or absolute - * @param {boolean} isAbsolute True for absolute URIs, false for relative URIs - * @param {{subString:string, value:string, property:string, selector:string}} bases Hash of possible base paths - * @return {string} Just the uri where base is empty or the uri appended to the base - */ - return function joinProper(filename, uri, isAbsolute, bases) { - var iterator = sanitiseIterable(generator(filename, uri, isAbsolute, bases, options)), - result = reduceIterator({inputs:[], outputs:[], isFound:false}, iterator), - lastOutput = result.outputs[result.outputs.length-1], - fallback = result.outputs.find(Boolean) || uri; - - log(debug.formatJoinMessage, [filename, uri, result.inputs, result.isFound]); - - return result.isFound ? lastOutput : fallback; - - /** - * Run the next iterator value. - * - * @param {Array} accumulator Current result - * @returns {Array} Updated result - */ - function reduceIterator(accumulator) { - var inputs = accumulator.inputs || [], - outputs = accumulator.outputs || [], - nextItem = iterator.next(); - - if (nextItem.done) { - return accumulator; - } else { - var base = assertAbsolute(nextItem.value, 'expected Iterator of absolute base path', ''), - pending = operation(filename, uri, base, next, options); - if (!!pending && typeof pending === 'object') { - return pending; - } else { - assertAbsolute(pending, 'operation must return an absolute path or the result of calling next()'); - return { - inputs : inputs.concat(base), - outputs: outputs.concat(pending), - isFound: true - }; - } + // run the iterator lazily and record attempts + const { isFileSync, isDirectorySync } = fsUtils(fs); + const attempts = []; + for (let i = 0; i < ITERATION_SAFETY_LIMIT; i++) { + const { value, done } = iterator.next(); + if (done) { + break; + } else if (value) { + const tuple = Array.isArray(value) && value.length === 2 ? value : null; + if (!tuple) { + throw new Error('expected Iterator values to be tuple of [string,string], do you need asGenerator utility?'); + } + + // skip elements where base or uri is non-string + // noting that we need to support base="" when root="" + const [base, uri] = value; + if ((typeof base === 'string') && (typeof uri === 'string')) { + + // validate + const isValidBase = (isAbsolute && base === root) || (path.isAbsolute(base) && isDirectorySync(base)); + if (!isValidBase) { + throw new Error(`expected "base" to be absolute path to a valid directory, got "${base}"`); } - /** - * Provide a possible fallback but run the next iteration either way. - * - * @param {string} fallback? Optional absolute path as fallback value - * @returns {Array} Nested result - */ - function next(fallback) { - assertAbsolute(fallback, 'next() expects absolute path string or no argument', null, undefined); - return reduceIterator({ - inputs : inputs.concat(base), - outputs: outputs.concat(fallback || []), - isFound: false - }); + // make the attempt + const joined = path.normalize(path.join(base, uri)); + const isFallback = true; + const isSuccess = isFileSync(joined); + attempts.push({base, uri, joined, isFallback, isSuccess}); + + if (isSuccess) { + break; } - /** - * Assert that the given value is an absolute path or some other accepted literal. - * - * @param {*} candidate Possible result - * @param {string} message Error message - * @param {...*} alsoAcceptable? Any number of simple values that are also acceptable - * @throws An error with the given message where the candidate fails the assertion - */ - function assertAbsolute(candidate, message, ...alsoAcceptable) { - var isValid = (alsoAcceptable.indexOf(candidate) >= 0) || - (typeof candidate === 'string') && path.isAbsolute(candidate); - if (!isValid) { - throw new Error(message); - } - return candidate; + // validate any non-strings are falsey + } else { + const isValidTuple = value.every((v) => (typeof v === 'string') || !v); + if (!isValidTuple) { + throw new Error('expected Iterator values to be tuple of [string,string]'); } } - }; + } } - function toString() { - return '[Function ' + name + ']'; - } + return attempts; +}; + +exports.createJoinImplementation = createJoinImplementation; + +/** + * A low-level utility to create a join function. + * + * The `implementation` function processes an individual `item` and returns an Array of attempts. Each attempt consists + * of a `base` and a `joined` value with `isSuccessful` and `isFallback` flags. + * + * In the case that any attempt `isSuccessful` then its `joined` value is the outcome. Otherwise the first `isFallback` + * attempt is used. If there is no successful or fallback attempts then `null` is returned indicating no change to the + * original URI in the CSS. + * + * The `attempts` Array is logged to console when in `debug` mode. + * + * @param {string} name Name for the resulting join function + * @param {function({uri:string, query:string, isAbsolute:boolean, bases:{subString:string, value:string, + * property:string, selector:string}}, {filename:string, fs:Object, debug:function|boolean, root:string}): + * Array<{base:string,joined:string,fallback?:string,result?:string}>} implementation A function accepts an item and + * returns a list of attempts + * @returns {function({filename:string, fs:Object, debug:function|boolean, root:string}): + * (function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string, + * selector:string}}):string)} join function + */ +const createJoinFunction = (name, implementation) => { + const assertAttempts = (value) => { + const isValid = + Array.isArray(value) && value.every((v) => + v && + (typeof v === 'object') && + (typeof v.base === 'string') && + (typeof v.uri === 'string') && + (typeof v.joined === 'string') && + (typeof v.isSuccess === 'boolean') && + (typeof v.isFallback === 'boolean') + ); + if (!isValid) { + throw new Error(`expected implementation to return Array of {base, uri, joined, isSuccess, isFallback}`); + } else { + return value; + } + }; + + const assertJoined = (value) => { + const isValid = value && (typeof value === 'string') && path.isAbsolute(value) || (value === null); + if (!isValid) { + throw new Error(`expected "joined" to be absolute path, got "${value}"`); + } else { + return value; + } + }; + + const join = (options, loader) => { + const { debug } = options; + const { resourcePath } = loader; + const log = createDebugLogger(debug); + + return (item) => { + const { uri } = item; + const attempts = implementation(item, options, loader); + assertAttempts(attempts, !!debug); + + const { joined: fallback } = attempts.find(({ isFallback }) => isFallback) || {}; + const { joined: result } = attempts.find(({ isSuccess }) => isSuccess) || {}; + + log(formatJoinMessage, [resourcePath, uri, attempts]); + + return assertJoined(result || fallback || null); + }; + }; + + const toString = () => '[Function ' + name + ']'; return Object.assign(join, !!name && { - toString: toString, - toJSON : toString + toString, + toJSON: toString }); -} +}; exports.createJoinFunction = createJoinFunction; + +/** + * The default iterable factory will order `subString` then `value` then `property` then `selector`. + * + * @param {string} uri The uri given in the file webpack is processing + * @param {boolean} isAbsolute True for absolute URIs, false for relative URIs + * @param {string} subString A possible base path + * @param {string} value A possible base path + * @param {string} property A possible base path + * @param {string} selector A possible base path + * @param {string} root The loader options.root value where given + * @returns {Array} An iterable of possible base paths in preference order + */ +const defaultJoinGenerator = asGenerator( + ({ uri, isAbsolute, bases: { subString, value, property, selector } }, { root }) => + isAbsolute ? [root] : [subString, value, property, selector] +); + +exports.defaultJoinGenerator = defaultJoinGenerator; + +/** + * @type {function({filename:string, fs:Object, debug:function|boolean, root:string}): + * (function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string, + * selector:string}}):string)} join function + */ +exports.defaultJoin = createJoinFunction( + 'defaultJoin', + createJoinImplementation(defaultJoinGenerator) +); diff --git a/packages/resolve-url-loader/lib/join-function/index.test.js b/packages/resolve-url-loader/lib/join-function/index.test.js index c7b731e..a2e15e4 100644 --- a/packages/resolve-url-loader/lib/join-function/index.test.js +++ b/packages/resolve-url-loader/lib/join-function/index.test.js @@ -9,209 +9,251 @@ const tape = require('blue-tape'); const sinon = require('sinon'); const outdent = require('outdent'); -const { createJoinFunction } = require('.'); -const { createDebugLogger, formatJoinMessage } = require('./debug'); -const sanitiseIterable = require('./sanitise-iterable'); - -const CURRENT_SCHEME = require('../../package.json').scheme; - -const json = (strings, ...substitutions) => - String.raw( - strings, - ...substitutions.map(v => JSON.stringify(v, (_, vv) => Number.isNaN(vv) ? 'NaN' : vv)) - ); +const { createJoinFunction, createJoinImplementation, asGenerator } = require('.'); tape( 'join-function', ({name, test, end: end1, equal, looseEqual, throws, doesNotThrow}) => { - test(`${name} / sanitiseIterable()`, ({end: end2}) => { - [ - [['a', 1, 'b', 2, 'c'], ['a', 'b', 'c']], - [['x', 'y', 'z', 'x', 'y'], ['x', 'y', 'z']] - ].forEach(([input, expected]) => { - const result = sanitiseIterable(input); + test(`${name} / createJoinImplementation()`, ({name: name2, test: test2, end: end2}) => { + const sandbox = sinon.createSandbox(); - equal( - result && typeof result === 'object' && typeof result[Symbol.iterator], - 'function', - 'Array input should create an Iterable output' - ); + const setup = () => { + const generator = sandbox.stub(); + const logFn = sandbox.spy(); + const isFile = sandbox.stub(); + const isDirectory = sandbox.stub(); + const fs = {statSync: () => ({isFile, isDirectory})}; + const item = {uri: 'my-asset.png', isAbsolute: false, query: '', bases: {}}; + const options = {debug: logFn}; + const loader = {resourcePath: 'my-source-file.js', fs}; + + return {generator, item, options, loader, logFn, isFile, isDirectory}; + }; + + test2(`${name2} / generator invocation`, ({end: end3}) => { + const {generator, item, options, loader} = setup(); + generator.returns([][Symbol.iterator]()); + + createJoinImplementation(generator)(item, options, loader); looseEqual( - [...result], - expected, - 'Array should be permitted and filtered for unique strings' + generator.args[0], + [item, options, loader], + 'should be called with expected arguments' ); + + end3(); }); - [ - [['a', 'b', 'c'][Symbol.iterator](), ['a', 'b', 'c']], - [[1, 2, 3][Symbol.iterator](), [1, 2, 3]], - [[1, 2, 3].keys(), [0, 1, 2]], // values() is unsupported until node v10.18.0 - [(function* () { yield 'x'; yield 'y'; yield 'z'; })(), ['x', 'y', 'z']], - ].forEach(([input, expected]) => - looseEqual( - [...sanitiseIterable(input)], - expected, - 'Iterable should be permitted and unfiltered' - ) - ); - - [ - false, - 123, - 'hello', - {a:1, b:2} - ].forEach((input) => + test2(`${name2} / generator validation`, ({end: end3}) => { + const {item, options, loader, isDirectory} = setup(); + + isDirectory.returns(true); + doesNotThrow( + () => createJoinImplementation(() => [[resolve('a'), 'b']][Symbol.iterator]())(item, options, loader), + 'Iterator factory function is permitted' + ); + + doesNotThrow( + () => createJoinImplementation(function* () { yield [resolve('a'), 'b']; })(item, options, loader), + 'Generator semantics are permitted' + ); + + doesNotThrow( + () => createJoinImplementation(asGenerator(() => [[resolve('a'), 'b']]))(item, options, loader), + 'asGenerator of an Array factory function is permitted' + ); + + doesNotThrow( + () => createJoinImplementation(() => [][Symbol.iterator]())(item, options, loader), + 'Empty iterator is permitted' + ); + + doesNotThrow( + () => [undefined, null, false, '', 0].forEach((v) => { + createJoinImplementation(() => [v][Symbol.iterator]())(item, options, loader); + }), + 'Tuples may be falsey' + ); + throws( - () => sanitiseIterable(input), - json`value ${input} should throw Error` - ) - ); + () => createJoinImplementation(() => [[]][Symbol.iterator]())(item, options, loader), + 'Tuples may not be empty' + ); - end2(); - }); + throws( + () => createJoinImplementation(() => [[resolve('a')]][Symbol.iterator]())(item, options, loader), + 'Tuples may not contain 1 element' + ); - test(`${name} / formatJoinMessage()`, ({end: end2}) => { - [ - // absolute within cwd - [ - [resolve('my-source-file.js'), 'my-asset.png', [resolve('foo'), resolve('bar', 'baz')], true], - outdent` - resolve-url-loader: ./my-source-file.js: my-asset.png - ./foo - ./bar/baz - FOUND - ` - ], - // absolute otherwise - [ - ['/my-source-file.js', '#anything\\./goes', ['/foo', '/bar/baz'], false], - outdent` - resolve-url-loader: /my-source-file.js: #anything\\./goes - /foo - /bar/baz - NOT FOUND - ` - ], - // presumed relative - [ - ['my-source-file.js', 'my-asset.png', ['foo', 'bar/baz'], true], - outdent` - resolve-url-loader: ./my-source-file.js: my-asset.png - ./foo - ./bar/baz - FOUND - ` - ], - // explicitly relative - [ - ['./my-source-file.js', 'my-asset.png', ['./foo', './bar/baz'], true], - outdent` - resolve-url-loader: ./my-source-file.js: my-asset.png - ./foo - ./bar/baz - FOUND - ` - ], - [ - ['../my-source-file.js', 'my-asset.png', ['../foo', '../bar/baz'], false], - outdent` - resolve-url-loader: ../my-source-file.js: my-asset.png - ../foo - ../bar/baz - NOT FOUND - ` - ], - // empty - [ - ['./my-source-file.js', 'my-asset.png', [''], true], - outdent` - resolve-url-loader: ./my-source-file.js: my-asset.png - -empty- - FOUND - ` - ], - ].forEach(([input, expected]) => - equal( - formatJoinMessage(...input), - expected, - json`input ${input} should sanitise to ${expected}` - ) - ); + throws( + () => createJoinImplementation(() => [[resolve('a'), 'b', 'c']][Symbol.iterator]())(item, options, loader), + 'Tuples may not contain 3 elements' + ); - end2(); - }); + throws( + () => [undefined, null, false, '', 0].forEach((v) => { + createJoinImplementation(() => [[v, 'b']][Symbol.iterator]())(item, options, loader); + }), + 'Base may not be falsey' + ); - test(`${name} / createDebugLogger()`, ({name: name2, test: test2, end: end2}) => { - test2(`${name2} / false`, ({end: end3}) => { - const sandbox = sinon.createSandbox(); - const factory = sandbox.fake.returns('foo'); - const consoleLog = sandbox.stub(console, 'log'); + doesNotThrow( + () => + createJoinImplementation(() => [['', 'b']][Symbol.iterator]()) + ({...item, isAbsolute: true}, {...options, root: ''}, loader), + 'Base may be "" where isAbsolute=true and root=""' + ); - const logger = createDebugLogger(false); - logger(factory); - sandbox.restore(); + doesNotThrow( + () => [undefined, null, false, '', 0].forEach((v) => { + createJoinImplementation(() => [[resolve('a'), v]][Symbol.iterator]())(item, options, loader); + }), + 'Uri may be falsey' + ); - equal(consoleLog.callCount, 0, 'should not call console.log()'); - equal(factory.callCount, 0, 'should not call underlying message factory'); + throws( + () => createJoinImplementation(() => [[{}, 'b']][Symbol.iterator]())(item, options, loader), + 'Thruthy base must be string' + ); + + throws( + () => createJoinImplementation(() => [[resolve('a'), {}]][Symbol.iterator]())(item, options, loader), + 'Thruthy uri must be string' + ); + + throws( + () => createJoinImplementation(() => [['a', 'b']][Symbol.iterator]())(item, options, loader), + 'Thruthy base must be absolute platform-specific path' + ); + + isDirectory.returns(false); + throws( + () => createJoinImplementation(() => [[resolve('a'), 'b']][Symbol.iterator]())(item, options, loader), + 'Thruthy base must be a valid directory' + ); end3(); }); - test2(`${name2} / true`, ({end: end3}) => { - const sandbox = sinon.createSandbox(); - const factory = sandbox.fake.returns('foo'); - const consoleLog = sandbox.stub(console, 'log'); + test2(`${name2} / immediate success`, ({end: end3}) => { + const {generator, item, options, loader, isDirectory, isFile} = setup(); + generator.returns([[resolve('a'), 'b'], [resolve('c'), 'd']][Symbol.iterator]()); + isDirectory.returns(true); + isFile.returns(true); - const logger = createDebugLogger(true); - logger(factory, ['bar', 1, false]); - logger(factory, ['baz', 2, undefined]); - sandbox.restore(); + const result = createJoinImplementation(generator)(item, options, loader); - equal(factory.callCount, 2, 'should call underlying log message factory'); looseEqual( - factory.args, - [['bar', 1, false], ['baz', 2, undefined]], - 'should call underlying message factory with expected arguments' + result, + [ + { + base: resolve('a'), + uri: 'b', + joined: resolve('a', 'b'), + isFallback: true, + isSuccess: true, + } + ], + 'should return the expected attempts list' ); - equal(consoleLog.callCount, 2, 'should call console.log()'); + end3(); + }); + + test2(`${name2} / fail then success`, ({end: end3}) => { + const {generator, item, options, loader, isDirectory, isFile} = setup(); + let callCount = 0; + generator.returns([[resolve('a'), 'b'], [resolve('c'), 'd']][Symbol.iterator]()); + isDirectory.returns(true); + isFile.callsFake(() => (callCount++ > 0)); + + const result = createJoinImplementation(generator)(item, options, loader); + looseEqual( - consoleLog.args, - [['foo'], ['foo']], - 'should log expected value' + result, + [ + { + base: resolve('a'), + uri: 'b', + joined: resolve('a', 'b'), + isFallback: true, + isSuccess: false, + }, { + base: resolve('c'), + uri: 'd', + joined: resolve('c', 'd'), + isFallback: true, + isSuccess: true, + } + ], + 'should return the expected attempts list' ); end3(); }); - test2(`${name2} / logFn`, ({end: end3}) => { - const sandbox = sinon.createSandbox(); - const factory = sandbox.fake.returns('foo'); - const consoleLog = sandbox.stub(console, 'log'); - const logFn = sandbox.spy(); + test2(`${name2} / failure`, ({end: end3}) => { + const {generator, item, options, loader, isDirectory, isFile} = setup(); + generator.returns([[resolve('a'), 'b'], [resolve('c'), 'd']][Symbol.iterator]()); + isDirectory.returns(true); + isFile.returns(false); - const logger = createDebugLogger(logFn); - logger(factory, ['bar', 1, false]); - logger(factory, ['baz', 2, undefined]); - sandbox.restore(); + const result = createJoinImplementation(generator)(item, options, loader); - equal(factory.callCount, 2, 'should call underlying log message factory'); looseEqual( - factory.args, - [['bar', 1, false], ['baz', 2, undefined]], - 'should call underlying message factory with expected arguments' + result, + [ + { + base: resolve('a'), + uri: 'b', + joined: resolve('a', 'b'), + isFallback: true, + isSuccess: false, + }, { + base: resolve('c'), + uri: 'd', + joined: resolve('c', 'd'), + isFallback: true, + isSuccess: false, + } + ], + 'should return the expected attempts list' ); - equal(logFn.callCount, 2, 'should call logFn()'); + end3(); + }); + + test2(`${name2} / empty`, ({end: end3}) => { + const {generator, item, options, loader, isDirectory, isFile} = setup(); + generator.returns([][Symbol.iterator]()); + isDirectory.returns(true); + isFile.returns(false); + + const result = createJoinImplementation(generator)(item, options, loader); + looseEqual( - logFn.args, - [['foo'], ['foo']], - 'should log expected value' + result, + [], + 'should return empty attempts list' ); - equal(consoleLog.callCount, 0, 'should not call console.log()'); + end3(); + }); + + test2(`${name2} / degenerate`, ({end: end3}) => { + const {generator, item, options, loader, isDirectory, isFile} = setup(); + generator.returns([[null, 'b'], [resolve('c'), null], null][Symbol.iterator]()); + isDirectory.returns(true); + isFile.returns(false); + + const result = createJoinImplementation(generator)(item, options, loader); + + looseEqual( + result, + [], + 'should return empty attempts list' + ); end3(); }); @@ -222,232 +264,222 @@ tape( test(`${name} / createJoinFunction()`, ({name: name2, test: test2, end: end2}) => { const sandbox = sinon.createSandbox(); - const setup = (scheme) => { - const generator = sandbox.stub(); - const operation = sandbox.stub(); + const setup = () => { + const implementation = sandbox.stub(); const logFn = sandbox.spy(); - const bases = {}; + const item = {uri: 'my-asset.png', isAbsolute: false, query: '', bases: {}}; const options = {debug: logFn}; + const loader = {resourcePath: 'my-source-file.js'}; - const sut = createJoinFunction({ - name: 'foo', - scheme, - generator, - operation - }); - - return {sut, generator, operation, bases, options, logFn}; + return {implementation, item, options, loader, logFn}; }; - test2(`${name2} / scheme`, ({end: end3}) => { - equal( - CURRENT_SCHEME, - CURRENT_SCHEME.toLowerCase(), - 'package.json scheme value is lowercase' + test2(`${name2} / implementation invocation`, ({end: end3}) => { + const {implementation, item, options, loader} = setup(); + implementation.returns([]); + + createJoinFunction('some-name', implementation)(options, loader)(item); + + looseEqual( + implementation.args[0], + [item, options, loader], + 'should be called with expected arguments' + ); + + end3(); + }); + + test2(`${name2} / implementation validation`, ({end: end3}) => { + const {item, options, loader} = setup(); + + throws( + () => createJoinFunction('some-name', () => 'foo')(options, loader)(item), + 'String is not is permitted' ); doesNotThrow( - () => setup(CURRENT_SCHEME), - 'should NOT throw on correct scheme' + () => createJoinFunction('some-name', () => [])(options, loader)(item), + 'Empty Array is permitted' + ); + + throws( + () => createJoinFunction('some-name', () => [1])(options, loader)(item), + 'Non-object elements is not permitted' ); doesNotThrow( - () => setup(CURRENT_SCHEME.toUpperCase()), - 'should NOT throw on correct scheme (uppercase)' + () => createJoinFunction('some-name', () => [ + {base: 'string', uri: 'string', joined: 'string', isSuccess:false, isFallback:false} + ])(options, loader)(item), + 'Object elements containing correct field types are permitted' + ); + + throws( + () => createJoinFunction('some-name', () => [ + {base: 1, uri: 'string', joined: 'string', isSuccess:false, isFallback:false} + ])(options, loader)(item), + 'Object elements containing incorrect field type are not permitted' ); throws( - () => setup('incorrect-scheme'), - 'should throw on mismatched scheme' + () => createJoinFunction('some-name', () => [ + {base: 'string', uri: 'string', joined: 'string', isSuccess:true, isFallback:false} + ])(options, loader)(item), + 'Object elements with non-absolute joined paths not permitted when isSuccess' + ); + + throws( + () => createJoinFunction('some-name', () => [ + {base: 'string', uri: 'string', joined: 'string', isSuccess:false, isFallback:true} + ])(options, loader)(item), + 'Object elements with non-absolute joined paths are not permitted when isFallback' + ); + + doesNotThrow( + () => [[false, false], [false, true], [true, false], [true, true]] + .forEach(([isSuccess, isFallback]) => createJoinFunction('some-name', () => [ + {base: 'string', uri: 'string', joined: resolve('a'), isSuccess, isFallback} + ])(options, loader)(item)), + 'Object elements with absolute joined paths are permitted' ); end3(); }); - test2(`${name2} / iterable`, ({end: end3}) => { - const {sut, generator, operation, bases, options} = setup(CURRENT_SCHEME); - generator.returns(['a', 'b', 'c'].map(v => resolve(v))); - operation.returns(resolve('bar')); + test2(`${name2} / immediate success`, ({end: end3}) => { + const {implementation, item, options, loader, logFn} = setup(); + implementation.returns([ + { + base: resolve('a'), + uri: 'b', + joined: resolve('a', 'b'), + isFallback: true, + isSuccess: true, + } + ]); - sut(options)('my-source-file.js', 'my-asset.png', false, bases); looseEqual( - generator.args[0], - ['my-source-file.js', 'my-asset.png', false, bases, options], - 'should be called with expected arguments' + createJoinFunction('some-name', implementation)(options, loader)(item), + resolve('a', 'b'), + 'should return the successful result' + ); + + looseEqual( + logFn.args[0], + [ + outdent` + resolve-url-loader: ./my-source-file.js: my-asset.png + ./a --> ./a/b + FOUND + ` + ], + 'should log the expected string' ); end3(); }); - test2(`${name2} / operation`, ({name: name3, test: test3, end: end3}) => { - const NEXT_ARG_INDEX = 3; - - const omitNextArg = (v) => - [...v.slice(0, NEXT_ARG_INDEX), ...v.slice(NEXT_ARG_INDEX+1)]; - - const setup2 = (fake) => { - const {sut, generator, operation, options, logFn} = setup(CURRENT_SCHEME); - let callCount = 0; - generator.returns(['a', 'b', 'c'].map(v => resolve(v))); - operation.callsFake((...args) => fake(callCount++, args[NEXT_ARG_INDEX])); - return {sut, generator, operation, options, logFn}; - }; - - test3(`${name3} / next(fallback) then success`, ({end: end4}) => { - const {sut, operation, bases, options, logFn} = setup2( - (i, next) => i === 0 ? next(resolve('foo')) : resolve('bar') - ); - - equal( - sut(options)('my-source-file.js', 'my-asset.png', false, bases), - resolve('bar'), - 'should return the expected result' - ); - - looseEqual( - operation.args.map(omitNextArg), - [ - ['my-source-file.js', 'my-asset.png', resolve('a'), options], - ['my-source-file.js', 'my-asset.png', resolve('b'), options] - ], - 'should be called with expected arguments' - ); - - looseEqual( - logFn.args, - [[outdent` - resolve-url-loader: ./my-source-file.js: my-asset.png - ./a - ./b - FOUND - `]], - 'should produce the expected debug message' - ); - - end4(); - }); - - test3(`${name3} / next() then success`, ({end: end4}) => { - const {sut, operation, bases, options, logFn} = setup2( - (i, next) => i === 0 ? next() : resolve('bar') - ); - - equal( - sut(options)('my-source-file.js', 'my-asset.png', false, bases), - resolve('bar'), - 'should return the expected result' - ); - - looseEqual( - operation.args.map(omitNextArg), - [ - ['my-source-file.js', 'my-asset.png', resolve('a'), options], - ['my-source-file.js', 'my-asset.png', resolve('b'), options] - ], - 'should be called with expected arguments' - ); - - looseEqual( - logFn.args, - [[outdent` - resolve-url-loader: ./my-source-file.js: my-asset.png - ./a - ./b - FOUND - `]], - 'should produce the expected debug message' - ); - - end4(); - }); - - test3(`${name3} / next(fallback) then next()`, ({end: end4}) => { - const {sut, operation, bases, options, logFn} = setup2( - (i, next) => i === 0 ? next(resolve('foo')) : next() - ); - - equal( - sut(options)('my-source-file.js', 'my-asset.png', false, bases), - resolve('foo'), - 'should return the expected result' - ); - - looseEqual( - operation.args.map(omitNextArg), - [ - ['my-source-file.js', 'my-asset.png', resolve('a'), options], - ['my-source-file.js', 'my-asset.png', resolve('b'), options], - ['my-source-file.js', 'my-asset.png', resolve('c'), options] - ], - 'should be called with expected arguments' - ); - - looseEqual( - logFn.args, - [[outdent` - resolve-url-loader: ./my-source-file.js: my-asset.png - ./a - ./b - ./c - NOT FOUND - `]], - 'should produce the expected debug message' - ); - - end4(); - }); - - test3(`${name3} / immediate success`, ({end: end4}) => { - const {sut, generator, operation, bases, options, logFn} = setup(CURRENT_SCHEME); - generator.returns(['a', 'b', 'c'].map(v => resolve(v))); - operation.callsFake(() => resolve('foo')); - - equal( - sut(options)('my-source-file.js', 'my-asset.png', false, bases), - resolve('foo'), - 'should return the expected result' - ); - - looseEqual( - operation.args.map(omitNextArg), - [ - ['my-source-file.js', 'my-asset.png', resolve('a'), options] - ], - 'should be called with expected arguments' - ); - - looseEqual( - logFn.args, - [[outdent` - resolve-url-loader: ./my-source-file.js: my-asset.png - ./a - FOUND - `]], - 'should produce the expected debug message' - ); - - end4(); - }); - - test3(`${name2} / output validation`, ({end: end4}) => { + test2(`${name2} / fail then success`, ({end: end3}) => { + const {implementation, item, options, loader, logFn} = setup(); + implementation.returns([ + { + base: resolve('a'), + uri: 'b', + joined: resolve('a', 'b'), + isFallback: true, + isSuccess: false, + }, { + base: resolve('c'), + uri: 'd', + joined: resolve('c', 'd'), + isFallback: true, + isSuccess: true, + } + ]); + + looseEqual( + createJoinFunction('some-name', implementation)(options, loader)(item), + resolve('c', 'd'), + 'should return the successful result' + ); + + looseEqual( + logFn.args[0], [ - [resolve('bar'), true], - ['bar', false], - ['#bar', false], - ['~bar', false], - ['~/bar', false] - ].forEach(([output, isValid]) => { - const {sut, generator, operation, bases, options} = setup(CURRENT_SCHEME); - generator.returns(['a', 'b', 'c'].map(v => resolve(v))); - operation.returns(output); - (isValid ? doesNotThrow : throws)( - () => sut(options)('my-source-file.js', 'my-asset.png', false, bases), - isValid ? json`should not throw on output ${output}` : json`should throw on output ${output}` - ); - }); - - end4(); - }); + outdent` + resolve-url-loader: ./my-source-file.js: my-asset.png + ./a --> ./a/b + ./c --> ./c/d + FOUND + ` + ], + 'should log the expected string' + ); + + end3(); + }); + + test2(`${name2} / failure`, ({end: end3}) => { + const {implementation, item, options, loader, logFn} = setup(); + implementation.returns([ + { + base: resolve('a'), + uri: 'b', + joined: resolve('a', 'b'), + isFallback: true, + isSuccess: false, + }, { + base: resolve('c'), + uri: 'd', + joined: resolve('c', 'd'), + isFallback: true, + isSuccess: false, + } + ]); + + looseEqual( + createJoinFunction('some-name', implementation)(options, loader)(item), + resolve('a', 'b'), + 'should return null' + ); + + looseEqual( + logFn.args[0], + [ + outdent` + resolve-url-loader: ./my-source-file.js: my-asset.png + ./a --> ./a/b + ./c --> ./c/d + NOT FOUND + ` + ], + 'should log the expected string' + ); + + end3(); + }); + + test2(`${name2} / empty`, ({end: end3}) => { + const {implementation, item, options, loader, logFn} = setup(); + implementation.returns([]); + + looseEqual( + createJoinFunction('some-name', implementation)(options, loader)(item), + null, + 'should return the expected result' + ); + + looseEqual( + logFn.args[0], + [ + outdent` + resolve-url-loader: ./my-source-file.js: my-asset.png + -empty- + NOT FOUND + ` + ], + 'should log the expected string' + ); end3(); }); diff --git a/packages/resolve-url-loader/lib/join-function/sanitise-iterable.js b/packages/resolve-url-loader/lib/join-function/sanitise-iterable.js deleted file mode 100644 index 4e260b4..0000000 --- a/packages/resolve-url-loader/lib/join-function/sanitise-iterable.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * MIT License http://opensource.org/licenses/MIT - * Author: Ben Holloway @bholloway - */ -'use strict'; - -/** - * A utility to ensure the given value is an Iterator. - * - * Where an Array is given its value are filtered to be Unique and Truthy. - * - * @throws TypeError where not Array or Iterator - * @param {Array|Iterator} candidate The value to consider - * @returns {Iterator} An iterator of string values - */ -function sanitiseIterable(candidate) { - if (Array.isArray(candidate)) { - return candidate.filter(isString).filter(isUnique)[Symbol.iterator](); - } else if (candidate && (typeof candidate === 'object') && candidate[Symbol.iterator]) { - return candidate; - } else { - throw new TypeError('Error in "join" function. Expected Array|Iterable'); - } - - function isString(v) { - return (typeof v === 'string'); - } - - function isUnique(v, i, a) { - return a.indexOf(v) === i; - } -} - -module.exports = sanitiseIterable; diff --git a/packages/resolve-url-loader/lib/value-processor.js b/packages/resolve-url-loader/lib/value-processor.js index 4b6cbcf..bf4b903 100644 --- a/packages/resolve-url-loader/lib/value-processor.js +++ b/packages/resolve-url-loader/lib/value-processor.js @@ -10,17 +10,15 @@ var path = require('path'), /** * Create a value processing function for a given file path. * - * @param {string} filename The current file being processed - * @param {{fs:Object, debug:function|boolean, join:function, root:string}} options Options hash + * @param {function(Object):string} join The inner join function + * @param {string} root The loader options.root value where given + * @param {string} directory The directory of the file webpack is currently processing * @return {function} value processing function */ -function valueProcessor(filename, options) { +function valueProcessor({ join, root, directory }) { var URL_STATEMENT_REGEX = /(url\s*\(\s*)(?:(['"])((?:(?!\2).)*)(\2)|([^'"](?:(?!\)).)*[^'"]))(\s*\))/g, QUERY_REGEX = /([?#])/g; - var directory = path.dirname(filename), - joinProper = options.join(options); - /** * Process the given CSS declaration value. * @@ -83,27 +81,29 @@ function valueProcessor(filename, options) { isQuoted = (before === after) && ((before === '\'') || (before === '"')), unescaped = isQuoted ? element.replace(/\\{2}/g, '\\') : element; - // split into uri and query/hash and then find the absolute path to the uri - // construct iterator as late as possible in case sourcemap is invalid at this location - var split = unescaped.split(QUERY_REGEX), - uri = split[0], - query = split.slice(1).join(''), - absolute = testIsRelative(uri) && joinProper(filename, uri, false, getPathsAtChar(position)) || - testIsAbsolute(uri) && joinProper(filename, uri, true, getPathsAtChar(position)); - - // not all URIs are files - if (!absolute) { - return element; - } else { - return loaderUtils.urlToRequest( - path.relative(directory, absolute).replace(/\\/g, '/') + query // #6 - backslashes are not legal in URI - ); + // split into uri and query/hash and then determine if the uri is some type of file + var split = unescaped.split(QUERY_REGEX), + uri = split[0], + query = split.slice(1).join(''), + isRelative = testIsRelative(uri), + isAbsolute = testIsAbsolute(uri); + + // file like URIs are processed but not all URIs are files + if (isRelative || isAbsolute) { + var bases = getPathsAtChar(position), // construct iterator as late as possible in case sourcemap invalid + absolute = join({ uri, query, isAbsolute, bases }); + + if (typeof absolute === 'string') { + var relative = path.relative(directory, absolute) + .replace(/\\/g, '/'); // #6 - backslashes are not legal in URI + + return loaderUtils.urlToRequest(relative + query); + } } } + // everything else, including parentheses and quotation (where present) and media statements - else { - return element; - } + return element; } }; @@ -128,7 +128,7 @@ function valueProcessor(filename, options) { * @return {boolean} True for absolute uri */ function testIsAbsolute(uri) { - return !!uri && (typeof options.root === 'string') && loaderUtils.isUrlRequest(uri, options.root) && + return !!uri && (typeof root === 'string') && loaderUtils.isUrlRequest(uri, root) && (/^\//.test(uri) || path.isAbsolute(uri)); } } diff --git a/packages/resolve-url-loader/package.json b/packages/resolve-url-loader/package.json index 1ee2eb9..8ff9261 100644 --- a/packages/resolve-url-loader/package.json +++ b/packages/resolve-url-loader/package.json @@ -52,6 +52,5 @@ "rework-visit": { "optional": true } - }, - "scheme": "alstroemeria" + } } From 4fe5cc3dd640a32fbe3d5cc7fb4dd0adbbb41764 Mon Sep 17 00:00:00 2001 From: Ben Holloway Date: Wed, 24 Mar 2021 21:38:07 +1100 Subject: [PATCH 3/5] adjust test cases for changed debug and error messages --- test/cases/absolute-asset.js | 2 +- test/cases/adjacent-asset.js | 2 +- test/cases/common/test/invalid.js | 38 ++++++++++++++++---- test/cases/declaration-mixin.postcss.js | 6 ++-- test/cases/declaration-variable.postcss.js | 12 +++---- test/cases/deep-asset.js | 2 +- test/cases/immediate-asset.js | 2 +- test/cases/misconfiguration.js | 26 +++++++++++--- test/cases/orphan-carriage-return.postcss.js | 2 +- test/cases/root-relative-asset.js | 2 +- test/cases/selector-in-directive.postcss.js | 4 +-- test/cases/shallow-asset.js | 2 +- 12 files changed, 71 insertions(+), 29 deletions(-) diff --git a/test/cases/absolute-asset.js b/test/cases/absolute-asset.js index 3744ac5..f6d8be8 100644 --- a/test/cases/absolute-asset.js +++ b/test/cases/absolute-asset.js @@ -19,7 +19,7 @@ const { const assertDebugMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]+${process.cwd()}.*${join('images', 'img.jpg')} - [ ]+-empty- + [ ]+-empty- --> ./images/img.jpg [ ]+FOUND$ `; diff --git a/test/cases/adjacent-asset.js b/test/cases/adjacent-asset.js index 10fd490..48fecf1 100644 --- a/test/cases/adjacent-asset.js +++ b/test/cases/adjacent-asset.js @@ -20,7 +20,7 @@ const { const assertDebugMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'../../../packageB/images/img.jpg'} - [ ]+${'./src/feature'} + [ ]+${'./src/feature'} --> ${'../packageB/images/img.jpg'} [ ]+FOUND$ `; diff --git a/test/cases/common/test/invalid.js b/test/cases/common/test/invalid.js index 3911d0e..e4ed175 100644 --- a/test/cases/common/test/invalid.js +++ b/test/cases/common/test/invalid.js @@ -71,32 +71,58 @@ exports.testFail = (...rest) => ) ); -exports.testNonFunctionJoin = (...rest) => +exports.testNonFunctionJoin1 = (...rest) => test( - 'join=!function', + 'join1=!function', layer()( env({ LOADER_JOIN: 'return 1;', - OUTPUT: 'non-function-join' + OUTPUT: 'non-function-join1' }), ...rest, test('validate', assertStderr('options.join')(1)`join: 1`) ) ); -exports.testWrongArityJoin = (...rest) => +exports.testWrongArityJoin1 = (...rest) => test( - 'join=!arity1', + 'join1=!arity2', + layer()( + env({ + LOADER_JOIN: 'return (a) => (b) => undefined;', + OUTPUT: 'wrong-arity-join1' + }), + ...rest, + test('validate', assertStderr('options.join')(1)`join: \(a\) => \(b\) => undefined`) + ) + ); + +exports.testNonFunctionJoin2 = (...rest) => + test( + 'join2=!function', layer()( env({ LOADER_JOIN: 'return (a, b) => undefined;', - OUTPUT: 'wrong-arity-join' + OUTPUT: 'non-function-join2' }), ...rest, test('validate', assertStderr('options.join')(1)`join: \(a, b\) => undefined`) ) ); +exports.testWrongArityJoin2 = (...rest) => + test( + 'join2=!arity', + layer()( + env({ + LOADER_JOIN: 'return (a, b) => (c, d) => undefined;', + OUTPUT: 'wrong-arity-join2' + }), + ...rest, + test('validate', assertStderr('options.join')(1)`join: \(a, b\) => \(c, d\) => undefined`) + ) + ); + exports.testNonStringRoot = (...rest) => test( 'root=!string', diff --git a/test/cases/declaration-mixin.postcss.js b/test/cases/declaration-mixin.postcss.js index aaecdfa..08cd437 100644 --- a/test/cases/declaration-mixin.postcss.js +++ b/test/cases/declaration-mixin.postcss.js @@ -20,14 +20,14 @@ const { const assertIncludeMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'img.jpg'} - [ ]+${'./src/feature'} - [ ]+${'./src'} + [ ]+${'./src/feature'} --> ${'./src/feature/img.jpg'} + [ ]+${'./src'} --> ${'./src/img.jpg'} [ ]+FOUND$ `; const assertMixinMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'img.jpg'} - [ ]+${'./src/feature'} + [ ]+${'./src/feature'} --> ${'./src/feature/img.jpg'} [ ]+FOUND$ `; diff --git a/test/cases/declaration-variable.postcss.js b/test/cases/declaration-variable.postcss.js index fb37ad1..0cf7b67 100644 --- a/test/cases/declaration-variable.postcss.js +++ b/test/cases/declaration-variable.postcss.js @@ -20,22 +20,22 @@ const { const assertPropertyMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'img.jpg'} - [ ]+${'./src/value/substring'} - [ ]+${'./src/value'} - [ ]+${'./src'} + [ ]+${'./src/value/substring'} --> ${'./src/value/substring/img.jpg'} + [ ]+${'./src/value'} --> ${'./src/value/img.jpg'} + [ ]+${'./src'} --> ${'./src/img.jpg'} [ ]+FOUND$ `; const assertValueMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'img.jpg'} - [ ]+${'./src/value/substring'} - [ ]+${'./src/value'} + [ ]+${'./src/value/substring'} --> ${'./src/value/substring/img.jpg'} + [ ]+${'./src/value'} --> ${'./src/value/img.jpg'} [ ]+FOUND$ `; const assertSubstringMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'img.jpg'} - [ ]+${'./src/value/substring'} + [ ]+${'./src/value/substring'} --> ${'./src/value/substring/img.jpg'} [ ]+FOUND$ `; diff --git a/test/cases/deep-asset.js b/test/cases/deep-asset.js index 594d0bb..acec865 100644 --- a/test/cases/deep-asset.js +++ b/test/cases/deep-asset.js @@ -20,7 +20,7 @@ const { const assertDebugMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'images/img.jpg'} - [ ]+${'./src/feature'} + [ ]+${'./src/feature'} --> ${'./src/feature/images/img.jpg'} [ ]+FOUND$ `; diff --git a/test/cases/immediate-asset.js b/test/cases/immediate-asset.js index 1c348f5..531d601 100644 --- a/test/cases/immediate-asset.js +++ b/test/cases/immediate-asset.js @@ -20,7 +20,7 @@ const { const assertDebugMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'img.jpg'} - [ ]+${'./src/feature'} + [ ]+${'./src/feature'} --> ${'./src/feature/img.jpg'} [ ]+FOUND$ `; diff --git a/test/cases/misconfiguration.js b/test/cases/misconfiguration.js index 31786b5..e1bd0f5 100644 --- a/test/cases/misconfiguration.js +++ b/test/cases/misconfiguration.js @@ -10,8 +10,8 @@ const {trim} = require('../lib/util'); const {rebaseToCache} = require('../lib/higher-order'); const { all, testDefault, testSilent, testKeepQuery, testAbsolute, testAttempts, testEngineFailInitialisation, - testEngineFailProcessing, testIncludeRoot, testFail, testNonFunctionJoin, testWrongArityJoin, testNonStringRoot, - testNonExistentRoot + testEngineFailProcessing, testIncludeRoot, testFail, testNonFunctionJoin1, testWrongArityJoin1, testNonFunctionJoin2, + testWrongArityJoin2, testNonStringRoot, testNonExistentRoot } = require('./common/test'); const {buildDevNormal, buildProdNormal} = require('./common/exec'); const {assertCssContent} = require('../lib/assert'); @@ -246,7 +246,7 @@ module.exports = test( ) ) ), - testNonFunctionJoin( + testNonFunctionJoin1( all(testDefault, testSilent)( all(buildDevNormal, buildProdNormal)( assertWebpackNotOk, @@ -254,11 +254,27 @@ module.exports = test( ) ) ), - testWrongArityJoin( + testWrongArityJoin1( all(testDefault, testSilent)( all(buildDevNormal, buildProdNormal)( assertWebpackNotOk, - assertMisconfigError('"join" Function must take exactly 1 arguments (options hash)') + assertMisconfigError('"join" Function must take exactly 2 arguments (options, loader)') + ) + ) + ), + testNonFunctionJoin2( + all(testDefault, testSilent)( + all(buildDevNormal, buildProdNormal)( + assertWebpackNotOk, + assertMisconfigError('"join" option must itself return a Function when it is called') + ) + ) + ), + testWrongArityJoin2( + all(testDefault, testSilent)( + all(buildDevNormal, buildProdNormal)( + assertWebpackNotOk, + assertMisconfigError('"join" Function must create a function that takes exactly 1 arguments (item)') ) ) ), diff --git a/test/cases/orphan-carriage-return.postcss.js b/test/cases/orphan-carriage-return.postcss.js index 9d23cfc..b6ae8eb 100644 --- a/test/cases/orphan-carriage-return.postcss.js +++ b/test/cases/orphan-carriage-return.postcss.js @@ -20,7 +20,7 @@ const { const assertDebugMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'img.jpg'} - [ ]+${'./src'} + [ ]+${'./src'} --> ${'./src/img.jpg'} [ ]+FOUND$ `; diff --git a/test/cases/root-relative-asset.js b/test/cases/root-relative-asset.js index 436bc94..5c395b7 100644 --- a/test/cases/root-relative-asset.js +++ b/test/cases/root-relative-asset.js @@ -20,7 +20,7 @@ const { const assertDebugMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'/images/img.jpg'} - [ ]+${'.'} + [ ]+${'.'} --> ${'./images/img.jpg'} [ ]+FOUND$ `; diff --git a/test/cases/selector-in-directive.postcss.js b/test/cases/selector-in-directive.postcss.js index cadd3bf..0a23d8c 100644 --- a/test/cases/selector-in-directive.postcss.js +++ b/test/cases/selector-in-directive.postcss.js @@ -22,12 +22,12 @@ const { const assertDebugMessages = sequence( assertStdout('debug')(4)` ^resolve-url-loader:[^:]+:[ ]*${'../fonts/font.'}\w+ - [ ]+${'./src/feature'} + [ ]+${'./src/feature'} --> ${'./src/fonts/font.'}\w+ [ ]+FOUND$ `, assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'images/img.jpg'} - [ ]+${'./src'} + [ ]+${'./src'} --> ${'./src/images/img.jpg'} [ ]+FOUND$ ` ); diff --git a/test/cases/shallow-asset.js b/test/cases/shallow-asset.js index e7aa1af..1b47e80 100644 --- a/test/cases/shallow-asset.js +++ b/test/cases/shallow-asset.js @@ -20,7 +20,7 @@ const { const assertDebugMessages = assertStdout('debug')(1)` ^resolve-url-loader:[^:]+:[ ]*${'../images/img.jpg'} - [ ]+${'./src/feature'} + [ ]+${'./src/feature'} --> ${'./src/images/img.jpg'} [ ]+FOUND$ `; From b96aba9d01ff975c77fc7f643d9e187f50cbfe5e Mon Sep 17 00:00:00 2001 From: Ben Holloway Date: Wed, 24 Mar 2021 23:21:22 +1100 Subject: [PATCH 4/5] adjust legacy filesearch for new API --- .../resolve-url-loader-filesearch/index.js.js | 117 ++++++++++-------- 1 file changed, 62 insertions(+), 55 deletions(-) diff --git a/packages/resolve-url-loader-filesearch/index.js.js b/packages/resolve-url-loader-filesearch/index.js.js index b2dcc1a..5eb2d3e 100644 --- a/packages/resolve-url-loader-filesearch/index.js.js +++ b/packages/resolve-url-loader-filesearch/index.js.js @@ -5,77 +5,84 @@ 'use strict'; const path = require('path'); -const { createJoinFunction, defaultJoinGenerator, defaultJoinOperation } = require('resolve-url-loader'); +const { createJoinFunction, createJoinImplementation, defaultJoinGenerator } = require('resolve-url-loader'); -module.exports = (options = {}) => { - const includeRoot = !!options.includeRoot; - const attempts = Math.max(0, options.attempts) || 1E+9; - const breakOnFile = [].concat(options.breakOnFile) || ['package.json', 'bower.json']; +const generator = function* (item, options, loader) { + if (item.isAbsolute) { + for (let tuple of defaultJoinGenerator(item, options, loader)) { + yield tuple; + } + } else { + const includeRoot = !!options.includeRoot; + const attempts = Math.max(0, options.attempts) || 1E+9; + const breakOnFile = [].concat(options.breakOnFile) || ['package.json', 'bower.json']; + const resolvedRoot = options.root || process.cwd(); - const predicate = typeof options.predicate === 'function' ? - options.testIsRunning : - ((fs, absolutePath) => breakOnFile - .map((file) => path.resolve(absolutePath, file)) - .every((file) => !fs.existsSync(file) || !fs.statSync(file).isFile())); + const isFile = (absolutePath) => { + try { + return loader.fs.statSync(absolutePath).isFile(); + } catch (error) { + return false; + } + }; + + const isDirectory = (absolutePath) => { + try { + return loader.fs.statSync(absolutePath).isDirectory(); + } catch (error) { + return false; + } + }; - const baseGenerator = typeof options.generator === 'function' ? - options.generator : - defaultJoinGenerator; + const predicate = typeof options.predicate === 'function' ? + options.predicate : + ((fs, absolutePath) => breakOnFile + .map((file) => path.resolve(absolutePath, file)) + .every((absolutePath) => !isFile(absolutePath))); - const searchingGeneartor = (filename, uri, bases, isAbsolute, {root, fs}) => { - const resolvedRoot = !!root && path.resolve(root) || process.cwd(); const testWithinLimit = (absolutePath) => { - var relative = path.relative(resolvedRoot, absolutePath); + const relative = path.relative(resolvedRoot, absolutePath); return !!relative && (relative.slice(0, 2) !== '..'); }; const enqueue = (queue, excludes, basePath) => - fs.readdirSync(basePath) + loader.fs.readdirSync(basePath) .filter((filename) => filename.charAt(0) !== '.') .map((filename) => path.join(basePath, filename)) - .filter((absolutePath) => fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) + .filter(isDirectory) .filter((absolutePath) => !excludes.contains(absolutePath)) - .filter((absolutePath) => predicate(fs, absolutePath)) + .filter((absolutePath) => predicate(loader.fs, absolutePath)) .forEach((absolutePath) => queue.push(absolutePath)); - return function* () { - for (let base of baseGenerator(filename, uri, bases, isAbsolute, options)) { - // #69 limit searching: make at least one attempt - let remaining = attempts; + for (let [absoluteStart, uri] of defaultJoinGenerator(item, options, loader)) { + // #69 limit searching: make at least one attempt + let remaining = attempts; - // find path to the root, stopping at cwd or at explicit boundary - const pathToRoot = []; - let isWorking; - let absoluteStart = path.resolve(base); - do { - pathToRoot.push(absoluteStart); - isWorking = testWithinLimit(absoluteStart) && predicate(absoluteStart); - absoluteStart = path.resolve(absoluteStart, '..'); - } while (isWorking); + // find path to the root, stopping at cwd or at explicit boundary + const pathToRoot = []; + let isWorking; + do { + pathToRoot.push(absoluteStart); + isWorking = testWithinLimit(absoluteStart) && predicate(absoluteStart); + absoluteStart = path.resolve(absoluteStart, '..'); + } while (isWorking); - // #62 support stylus nib: optionally force that path to include the root - const appendRoot = includeRoot && (pathToRoot.indexOf(resolvedRoot) < 0); - const queue = pathToRoot.concat(appendRoot ? resolvedRoot : []); + // #62 support stylus nib: optionally force that path to include the root + const appendRoot = includeRoot && (pathToRoot.indexOf(resolvedRoot) < 0); + const queue = pathToRoot.concat(appendRoot ? resolvedRoot : []); - // the queue pattern ensures that we favour paths closest the the start path - // process the queue until empty or until we exhaust our attempts - while (queue.length && (remaining-- > 0)) { - var basePath = queue.shift(); - yield basePath; - enqueue(queue, pathToRoot, basePath); - } + // the queue pattern ensures that we favour paths closest the the start path + // process the queue until empty or until we exhaust our attempts + while (queue.length && (remaining-- > 0)) { + const base = queue.shift(); + yield [base, uri]; + enqueue(queue, pathToRoot, base); } - }; - }; - - // only decorate relative URIs - const generator = (filename, uri, bases, isAbsolute, options) => - (isAbsolute ? baseGenerator : searchingGeneartor)(filename, uri, bases, isAbsolute, options); - - return createJoinFunction({ - name: 'resolve-url-loader-filesearch', - scheme: 'alstroemeria', - generator: generator, - operation: defaultJoinOperation - }); + } + } }; + +module.exports = createJoinFunction( + 'resolve-url-loader-filesearch', + createJoinImplementation(generator) +); From d5ea53c99c99217bb8aa42db1748cdd52f077279 Mon Sep 17 00:00:00 2001 From: Ben Holloway Date: Thu, 25 Mar 2021 17:21:57 +1100 Subject: [PATCH 5/5] fix minor gramatical errors --- packages/resolve-url-loader/docs/advanced-features.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/resolve-url-loader/docs/advanced-features.md b/packages/resolve-url-loader/docs/advanced-features.md index ebc4038..85114c9 100644 --- a/packages/resolve-url-loader/docs/advanced-features.md +++ b/packages/resolve-url-loader/docs/advanced-features.md @@ -14,7 +14,7 @@ The "join" function determines how CSS URIs are combined with one of the possibl ⚠️ **IMPORTANT** - First read how the [algorithm](./how-it-works.md#algorithm) works. -The "join" function is higher-order function created using the `options` and `loader` reference. That gives a function that accepts a single `item` and synchronously returns an absolute asset path to substitute back into the original CSS. +The "join" function is a higher-order function created using the `options` and `loader` reference. That gives a function that accepts a single `item` and synchronously returns an absolute asset path to substitute back into the original CSS. ```javascript (options:{}, loader:{}) => @@ -32,7 +32,7 @@ A custom `join` function from scratch is possible but we've provided some [build ## Building blocks -There are number of utilities (defined in [`lib/join-function/index.js`](../lib/join-function/index.js)) to help construct a custom "join" function . These are conveniently re-exported as properties of the loader. +There are a number of utilities (defined in [`lib/join-function/index.js`](../lib/join-function/index.js)) to help construct a custom "join" function . These are conveniently re-exported as properties of the loader. These utilities are used to create the `defaultJoin` as follows. @@ -120,7 +120,7 @@ When using `asGenerator` you may return elements as either `base:string` **or** That said there are cases where you might want to amend the `uri`. The solution is to make each element a tuple of `base` and `uri` representing a potential location to find the asset. - If you'e interested only in the `base` path and don't intend to vary the `uri` then the `asGenerator` utility saves you having to create repetiative tuples (and from using `function*` semantics). + If you're interested only in the `base` path and don't intend to vary the `uri` then the `asGenerator` utility saves you having to create repetative tuples (and from using `function*` semantics). * **Can I vary the `query` using the tuple?**