source: trip-planner-front/node_modules/resolve-url-loader/docs/advanced-features.md@ 571e0df

Last change on this file since 571e0df was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 17.2 KB
Line 
1# Advanced Features
2
3All the advanced features of this loader involve customising the `join` option.
4
5Jump to the **"how to"** section -
6 * [How to: change precedence of source locations](#how-to-change-precedence-of-source-locations)
7 * [How to: fallback to a theme or other global directory](#how-to-fallback-to-a-theme-or-other-global-directory)
8 * [How to: fallback to some other asset file](#how-to-fallback-to-some-other-asset-file)
9 * [How to: perform a file-system search for an asset](#how-to-perform-a-file-system-search-for-an-asset)
10
11## What is the "join" function?
12
13The "join" function determines how CSS URIs are combined with one of the possible base paths the algorithm has identified.
14
15⚠️ **IMPORTANT** - First read how the [algorithm](./how-it-works.md#algorithm) works.
16
17The "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.
18
19```javascript
20(options:{}, loader:{}) =>
21 (item:{ uri:string, query: string, isAbsolute: boolean, bases:{} }) =>
22 string | null
23```
24
25Where 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.
26
27The 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.
28
29The default implementation is suitable for most users but can be customised per the `join` option.
30
31A custom `join` function from scratch is possible but we've provided some [building blocks](#building-blocks) to make the task easier.
32
33## Building blocks
34
35There 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.
36
37These utilities are used to create the `defaultJoin` as follows.
38
39```javascript
40const {
41 createJoinFunction,
42 createJoinImplementation,
43 defaultJoinGenerator,
44} = require('resolve-url-loader');
45
46// create a join function equivalent to "defaultJoin"
47const myJoinFn = createJoinFunction(
48 'myJoinFn',
49 createJoinImplementation(defaultJoinGenerator),
50});
51```
52
53🤓 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.
54
55```javascript
56createJoinFunction = (name:string, implementation: function): function
57```
58
59For 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.
60
61👉 However its recommended to instead use `createJoinImplementation` to create the `implementation` using the `generator` concept.
62
63```javascript
64createJoinImplementation = (generator: function*): function
65```
66
67The `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.
68
69Don't worry, you don't need to use `function*` semantics for the `generator` unless you want to.
70
71## Simple customisation
72
73It 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`.
74
75See the reference or jump directly to the [examples](#how-to-change-precedence-of-source-locations).
76
77### Reference
78
79The `generator` identifies `[base:string,uri:string]` tuples describing locations to search for an asset. It does **not** return the final asset path.
80
81You 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).
82```javascript
83generator = function* (item: {}, options: {}, loader: {}): Iterator<[string,string]>
84```
85
86Or it can be simpler to write a function that returns `Array` and convert it to a generator using `asGenerator`.
87
88```javascript
89generator = asGenerator( function (item: {}, options: {}, loader: {}): Array<string> )
90```
91```javascript
92generator = asGenerator( function (item: {}, options: {}, loader: {}): Array<[string,string]> )
93```
94
95When using `asGenerator` you may return elements as either `base:string` **or** `[base:string,uri:string]` tuples.
96
97<details>
98<summary>Arguments</summary>
99
100* `item` consist of -
101 * `uri: string` is the argument to the `url()` as it appears in the source file.
102 * `query: string` is any query or hash string starting with `?` or `#` that suffixes the `uri`
103 * `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.
104 * `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.
105* `options` consist of -
106 * All documented options for the loader.
107 * Any other values you include in the loader configuration for your own purposes.
108* `loader` consists of the webpack loader API, useful items include -
109 * `fs: {}` the virtual file-system from Webpack.
110 * `resourcePath: string` the source file currently being processed.
111* returns an `Iterator` with elements of `[base:string,uri:string]` either intrinsically or by using `asGenerator`.
112</details>
113
114<details>
115<summary>FAQ</summary>
116
117* **Why a tuple?**
118
119 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.
120
121 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.
122
123 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).
124
125* **Can I vary the `query` using the tuple?**
126
127 No. We don't support amending the `query` in the final value. If you would like this enhancement please open an issue.
128
129* **What about duplicate or falsey elements?**
130
131 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.
132
133 If you use `Array` then `asGenerator` will also remove duplicates.
134
135* **When should I use `function*`?**
136
137 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).
138
139 But in most cases, when the values are known apriori, simply returning `Array` has simpler semantics making `asGenerator` preferable.
140
141* **Why is this generator so complicated?**
142
143 The join function must make multiple attempts to join a `base` and `uri` and check that the file exists using webpack `fs`.
144
145 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.
146
147 If you prefer a less abstract approach consider a full `implementation` per the [full customisation](#full-customisation) approach.
148</details>
149
150### How to: change precedence of source locations
151
152Source-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.
153
154This example shows the default order which you can easily amend. Absolute URIs are rare in most projects but can be handled for completeness.
155
156**Using `asGenerator`**
157
158```javascript
159const {
160 createJoinFunction,
161 createJoinImplementation,
162 asGenerator,
163 defaultJoinGenerator,
164} = require('resolve-url-loader');
165
166// order source-map sampling location by your preferred precedence (matches defaultJoinGenerator)
167const myGenerator = asGenerator(
168 ({ isAbsolute, bases: { substring, value, property, selector} }, { root }) =>
169 isAbsolute ? [root] : [subString, value, property, selector]
170);
171
172const myJoinFn = createJoinFunction(
173 'myJoinFn',
174 createJoinImplementation(myGenerator),
175);
176```
177
178**Notes**
179
180* The implementation is the default behaviour, so if you want this precedence do **not** customise the `join` option.
181* Absolute URIs generally use the base path given in the `root` option as shown.
182* The `asGenerator` utility allows us to return simple `Array<string>` of potential base paths.
183
184### How to: fallback to a theme or other global directory
185
186Additional locations can be added by decorating the default generator. This is popular for adding some sort of "theme" directory containing assets.
187
188This 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.
189
190**Using `asGenerator`**
191
192```javascript
193const path = require('path');
194const {
195 createJoinFunction,
196 createJoinImplementation,
197 asGenerator,
198 defaultJoinGenerator,
199} = require('resolve-url-loader');
200
201const myThemeDirectory = path.resolve(...);
202
203// call default generator then append any additional paths
204const myGenerator = asGenerator(
205 (item, ...rest) => [
206 ...defaultJoinGenerator(item, ...rest),
207 item.isAbsolute ? null : myThemeDirectory,
208 ]
209);
210
211const myJoinFn = createJoinFunction(
212 'myJoinFn',
213 createJoinImplementation(myGenerator),
214);
215```
216
217**Notes**
218
219* By spreading the result of `defaultJoinGenerator` we are first trying the default behaviour. If that is unsuccessful we then try the theme location.
220* 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.
221* The `asGenerator` utility allows us to return simple `Array<string>` of potential base paths.
222
223### How to: fallback to some other asset file
224
225Lets 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]`.
226
227In this example we prefer the `.svg` asset we are happy to use any available `.png` or `.jpg` instead.
228
229**Using `asGenerator`**
230
231```javascript
232const {
233 createJoinFunction,
234 createJoinImplementation,
235 asGenerator,
236 defaultJoinGenerator,
237} = require('resolve-url-loader');
238
239// call default generator then pair different variations of uri with each base
240const myGenerator = asGenerator(
241 (item, ...rest) => {
242 const defaultTuples = [...defaultJoinGenerator(item, ...rest)];
243 return /\.svg$/.test(item.uri)
244 ? ['.svg', '.png', 'jpg'].flatMap((ext) =>
245 defaultTuples.flatMap(([base, uri]) =>
246 [base, uri.replace(/\.svg$/, ext)]
247 })
248 )
249 : defaultTuples;
250 }
251);
252
253const myJoinFn = createJoinFunction(
254 'myJoinFn',
255 createJoinImplementation(myGenerator),
256);
257```
258
259**Using `function*`**
260
261```javascript
262const {
263 createJoinFunction,
264 createJoinImplementation,
265 defaultJoinGenerator,
266} = require('resolve-url-loader');
267
268// call default generator then pair different variations of uri with each base
269const myGenerator = function* (item, ...rest) {
270 if (/\.svg$/.test(item.uri)) {
271 for (let ext of ['.svg', '.png', 'jpg']) {
272 for (let [base, uri] of defaultJoinGenerator(item, ...rest)) {
273 yield [base, uri.replace(/\.svg$/, ext)];
274 }
275 }
276 } else {
277 for (let value of defaultJoinGenerator(item, ...rest)) {
278 yield value;
279 }
280 }
281}
282
283const myJoinFn = createJoinFunction(
284 'myJoinFn',
285 createJoinImplementation(myGenerator),
286);
287```
288
289**Notes**
290
291* Existing generators such as `defaultJoinGenerator` will always return `[string,string]` tuples so we can destruture `base` and `uri` values with confidence.
292* 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
293* The `asGenerator` utility allows us to return `Array<[string, string]>` but is **not** needed when we use `function*` semantics.
294
295### How to: perform a file-system search for an asset
296
297⚠️ **IMPORTANT** - This example is indicative only and is **not** advised.
298
299When 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.
300
301In 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.
302
303**Using `function*`**
304
305```javascript
306const path = require('path');
307const {
308 createJoinFunction,
309 createJoinImplementation,
310 webpackExistsSync
311} = require('resolve-url-loader');
312
313// search up from the initial base path until you hit a package boundary
314const myGenerator = function* (
315 { uri, isAbsolute, bases: { substring, value, property, selector } },
316 { root, attempts = 1e3 },
317 { fs },
318) {
319 if (isAbsolute) {
320 yield [root, uri];
321 } else {
322 for (let base of [subString, value, property, selector]) {
323 for (let isDone = false, i = 0; !isDone && i < attempts; i++) {
324 yield [base, uri];
325 // unfortunately fs.existsSync() is not present so we must shim it
326 const maybePkg = path.normalize(path.join(base, 'package.json'));
327 try {
328 isDone = fs.statSync(maybePkg).isFile();
329 } catch (error) {
330 isDone = false;
331 }
332 base = base.split(/(\\\/)/).slice(0, -2).join('');
333 }
334 }
335 }
336};
337
338const myJoinFn = createJoinFunction(
339 'myJoinFn',
340 createJoinImplementation(myGenerator),
341);
342```
343
344**Notes**
345
346* This implementation is nether tested nor robust, it would need further safeguards to avoid searching the entire file system.
347
348* By using `function*` the generator is lazy. We only walk the file-system directory tree as necessary.
349
350* 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.
351
352* 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.
353
354
355## Full customisation
356
357The `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.
358
359It provides additional logging when using `debug` option so is a better choice then writing a "join" function from scratch.
360
361Limited documentation is given here since it is rare to require a full customisation. Refer to the source code for further information.
362
363### Reference
364
365The `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.
366
367```javascript
368implementation = function (item: {}, options: {}, loader: {}):
369 Array<{
370 base : string,
371 uri : string,
372 joined : string,
373 isSuccess : boolean,
374 isFallback: boolean,
375 }>
376```
377<details>
378<summary>Arguments</summary>
379
380* `item` consist of -
381 * `uri: string` is the argument to the `url()` as it appears in the source file.
382 * `query: string` is any string starting with `?` or `#` that suffixes the `uri`
383 * `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.
384 * `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.
385* `options` consist of -
386 * All documented options for the loader.
387 * Any other values you include in the loader configuration for your own purposes.
388* `loader` consists of the webpack loader API, useful items include -
389 * `fs: {}` the virtual file-system from Webpack.
390 * `resourcePath: string` the source file currently being processed.
391* returns an array of attempts that were made in resolving the URI -
392 * `base` the base path
393 * `uri` the uri path
394 * `joined` the absolute path created from the joining the `base` and `uri` paths.
395 * `isSuccess` indicates the asset was found and that `joined` should be the final result
396 * `isFallback` indicates the asset was not found but that `joined` kis suitable as a fallback value
397</details>
Note: See TracBrowser for help on using the repository browser.