├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .nojekyll ├── .npmignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── icons.js │ ├── icons.svg │ ├── main.js │ ├── material-style.css │ ├── navigation.js │ ├── search.js │ └── style.css ├── classes │ └── index.AsyncPreloader.html ├── enums │ └── types.LoaderKey.html ├── index.html ├── interfaces │ ├── types.FontOptions.html │ ├── types.LoadItem.html │ └── types.LoaderValue.html ├── modules │ ├── index.html │ └── types.html ├── types │ ├── types.BodyMethod.html │ ├── types.BodyResolveValue.html │ ├── types.LoadedValue.html │ └── types.LoadedXMLValue.html └── variables │ └── index.default.html ├── index.html ├── package-lock.json ├── package.json ├── src ├── index.ts └── types.ts ├── test ├── assets │ ├── audio.mp3 │ ├── default.unknown │ ├── font.ttf │ ├── image.gif │ ├── image.jpg │ ├── image.png │ ├── json.json │ ├── manifest.json │ ├── text.txt │ ├── video.mp4 │ ├── xml.html │ ├── xml.svg │ ├── xml.unknown │ └── xml.xml ├── browser.spec.js ├── data.js ├── global-setup.js ├── global-teardown.js ├── index.spec.js └── server.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | test: 11 | name: Node.js ${{ matrix.node-version }} 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 5 14 | strategy: 15 | matrix: 16 | node_version: ["22"] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node_version }} 22 | cache: "npm" 23 | - run: npm ci --verbose 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | /types 4 | /lib 5 | coverage 6 | /web_modules 7 | .nyc_output 8 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/async-preloader/c1a85109afec2a2500fc29044601caef31825333/.nojekyll -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /web_modules 2 | /examples 3 | /docs 4 | /coverage 5 | /test 6 | /.github 7 | screenshot.* 8 | index.html 9 | tsconfig.json 10 | .editorconfig 11 | .nojekyll 12 | .nyc_output 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. 4 | 5 | ## [8.0.4](https://github.com/dmnsgn/async-preloader/compare/v8.0.3...v8.0.4) (2024-07-08) 6 | 7 | 8 | 9 | ## [8.0.3](https://github.com/dmnsgn/async-preloader/compare/v8.0.2...v8.0.3) (2023-11-02) 10 | 11 | 12 | 13 | ## [8.0.2](https://github.com/dmnsgn/async-preloader/compare/v8.0.1...v8.0.2) (2023-02-09) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * force @types/css-font-loading-module to 0.07 and update tsconfig target ([4ca26f9](https://github.com/dmnsgn/async-preloader/commit/4ca26f9f51ffb28b025c49c7cee28e5e1f6fc670)) 19 | 20 | 21 | 22 | ## [8.0.1](https://github.com/dmnsgn/async-preloader/compare/v8.0.0...v8.0.1) (2023-02-09) 23 | 24 | 25 | 26 | # [8.0.0](https://github.com/dmnsgn/async-preloader/compare/v7.0.0...v8.0.0) (2022-10-25) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * check for unsupported features in nodes ([5ad3ae6](https://github.com/dmnsgn/async-preloader/commit/5ad3ae6e2c7e613f4c2be6d6d6a6830ec92801b2)) 32 | * reject with error event in loadAudio/Video ([5735f9a](https://github.com/dmnsgn/async-preloader/commit/5735f9a6bf040308bbc436a828e5f39d1aa30fa0)) 33 | 34 | 35 | ### Features 36 | 37 | * add noDecode ([7394e0d](https://github.com/dmnsgn/async-preloader/commit/7394e0d200a546d1c480866c3d8d5a711d5d1d2a)) 38 | * allow string for loadItem(s) ([7025c7d](https://github.com/dmnsgn/async-preloader/commit/7025c7d95f885b01c1121e1e8de877b61aa8ca81)) 39 | * use HTMLImageElement.decode() ([2388d40](https://github.com/dmnsgn/async-preloader/commit/2388d40d5fd134efab2ea01a67acb1574d91be64)), closes [#89](https://github.com/dmnsgn/async-preloader/issues/89) 40 | 41 | 42 | ### BREAKING CHANGES 43 | 44 | * loadImage now decodes the image by default 45 | 46 | 47 | 48 | # [7.0.0](https://github.com/dmnsgn/async-preloader/compare/v6.1.2...v7.0.0) (2022-06-13) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * exclude android and chrome from isSafari regex ([acf8770](https://github.com/dmnsgn/async-preloader/commit/acf87707d3a6cbb099b990b88a1bb4de42d7c4d6)) 54 | 55 | 56 | ### BREAKING CHANGES 57 | 58 | * changes audio and video loading in Chrome 59 | 60 | 61 | 62 | ## [6.1.2](https://github.com/dmnsgn/async-preloader/compare/v6.1.1...v6.1.2) (2022-04-02) 63 | 64 | 65 | 66 | ## [6.1.1](https://github.com/dmnsgn/async-preloader/compare/v6.1.0...v6.1.1) (2022-01-14) 67 | 68 | 69 | 70 | # [6.1.0](https://github.com/dmnsgn/async-preloader/compare/v6.0.0...v6.1.0) (2022-01-14) 71 | 72 | 73 | ### Features 74 | 75 | * compute font name from src if no id is present ([6703830](https://github.com/dmnsgn/async-preloader/commit/6703830085a40a360e5ce6539614d2361c1c38ec)) 76 | 77 | 78 | 79 | # [6.0.0](https://github.com/dmnsgn/async-preloader/compare/v5.2.1...v6.0.0) (2021-12-04) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * enhance node support by checking for navigator and DOMParser ([9e4f6c5](https://github.com/dmnsgn/async-preloader/commit/9e4f6c555ea82950d446f4db0146c0ffe99db9c4)) 85 | * handle falsy path in getFileExtension ([aadd369](https://github.com/dmnsgn/async-preloader/commit/aadd36949ce2fbec3fa2aa23b782be35e17ad672)) 86 | * update returned types for loaders ([fe46654](https://github.com/dmnsgn/async-preloader/commit/fe4665488645f1b3708afaeaf7dfc13d0d416f5c)) 87 | 88 | 89 | ### Features 90 | 91 | * add support for FontFace ([3830dd7](https://github.com/dmnsgn/async-preloader/commit/3830dd78a0231e3fc87e452036403f27bf2509e8)), closes [#79](https://github.com/dmnsgn/async-preloader/issues/79) 92 | 93 | 94 | ### BREAKING CHANGES 95 | 96 | * new behaviour for loadFont 97 | 98 | 99 | 100 | ## [5.2.1](https://github.com/dmnsgn/async-preloader/compare/v5.2.0...v5.2.1) (2021-11-12) 101 | 102 | 103 | 104 | # [5.2.0](https://github.com/dmnsgn/async-preloader/compare/v5.1.0...v5.2.0) (2021-10-29) 105 | 106 | 107 | ### Features 108 | 109 | * update dependencies and ts-jest ([6a452ff](https://github.com/dmnsgn/async-preloader/commit/6a452ffd35f11e5085d4b3945c180b81fb933815)) 110 | 111 | 112 | 113 | # [5.1.0](https://github.com/dmnsgn/async-preloader/compare/v5.0.2...v5.1.0) (2021-10-02) 114 | 115 | 116 | ### Features 117 | 118 | * add exports field to package.json ([f9689de](https://github.com/dmnsgn/async-preloader/commit/f9689de1dada6c2b0140a1e5821c4eb869ee47d8)) 119 | 120 | 121 | 122 | ## [5.0.2](https://github.com/dmnsgn/async-preloader/compare/v5.0.1...v5.0.2) (2021-03-24) 123 | 124 | 125 | 126 | ## [5.0.1](https://github.com/dmnsgn/async-preloader/compare/v5.0.0...v5.0.1) (2021-03-20) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * update npmignore ([b0d8e64](https://github.com/dmnsgn/async-preloader/commit/b0d8e64d4c4de183c9e61f78feab21731cd8f1e0)) 132 | 133 | 134 | 135 | # [5.0.0](https://github.com/dmnsgn/async-preloader/compare/v4.9.2...v5.0.0) (2021-03-20) 136 | 137 | 138 | ### Code Refactoring 139 | 140 | * use ES modules ([c171d01](https://github.com/dmnsgn/async-preloader/commit/c171d0178f27b5e04fff7ea02260517b062e9e24)) 141 | 142 | 143 | ### BREAKING CHANGES 144 | 145 | * remove cjs/umd 146 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 Damien Seguin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-preloader 2 | 3 | [![npm version](https://img.shields.io/npm/v/async-preloader)](https://www.npmjs.com/package/async-preloader) 4 | [![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)](https://www.npmjs.com/package/async-preloader) 5 | [![npm minzipped size](https://img.shields.io/bundlephobia/minzip/async-preloader)](https://bundlephobia.com/package/async-preloader) 6 | [![dependencies](https://img.shields.io/librariesio/release/npm/async-preloader)](https://github.com/dmnsgn/async-preloader/blob/main/package.json) 7 | [![types](https://img.shields.io/npm/types/async-preloader)](https://github.com/microsoft/TypeScript) 8 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fa6673.svg)](https://conventionalcommits.org) 9 | [![styled with prettier](https://img.shields.io/badge/styled_with-Prettier-f8bc45.svg?logo=prettier)](https://github.com/prettier/prettier) 10 | [![linted with eslint](https://img.shields.io/badge/linted_with-ES_Lint-4B32C3.svg?logo=eslint)](https://github.com/eslint/eslint) 11 | [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) 12 | [![license](https://img.shields.io/github/license/dmnsgn/async-preloader)](https://github.com/dmnsgn/async-preloader/blob/main/LICENSE.md) 13 | 14 | Assets preloader using async/await and fetch for usage both in the browser and Node.js. 15 | 16 | [![paypal](https://img.shields.io/badge/donate-paypal-informational?logo=paypal)](https://paypal.me/dmnsgn) 17 | [![coinbase](https://img.shields.io/badge/donate-coinbase-informational?logo=coinbase)](https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3) 18 | [![twitter](https://img.shields.io/twitter/follow/dmnsgn?style=social)](https://twitter.com/dmnsgn) 19 | 20 | ## Install 21 | 22 | ```bash 23 | npm install --save async-preloader 24 | ``` 25 | 26 | ## Documentation 27 | 28 | - [AsyncPreloader class](https://dmnsgn.github.io/async-preloader/classes/index.AsyncPreloader.html) 29 | - [AsyncPreloader types](https://dmnsgn.github.io/async-preloader/modules/types.html) 30 | 31 | ## Quick start 32 | 33 | This section covers the basic usage of `AsyncPreloader`. For more informations about async/await, see [Async functions - making promises friendly](https://developers.google.com/web/fundamentals/primers/async-functions). Usage in Node.js environment is limited to its capacity to handle `fetch` requests and DOM APIs. Polyfills like [`undici`](https://www.npmjs.com/package/undici/)/[`node-fetch`](https://www.npmjs.com/package/node-fetch) (for Node.js below 18) and [`xmldom`](https://www.npmjs.com/package/xmldom) might come handy . 34 | 35 | ### Preload items and retrieve them 36 | 37 | ```javascript 38 | import AsyncPreloader from "async-preloader"; 39 | 40 | const items = [ 41 | { id: "myDefaultFile", src: "assets/default" }, 42 | { id: "myTextFile", src: "assets/text.txt" }, 43 | { id: "myJsonFile", src: "assets/json.json" }, 44 | { id: "myImageFile", src: "assets/image.jpg" }, 45 | { id: "myVideoFile", src: "assets/video.mp4" }, 46 | { id: "myAudioFile", src: "assets/audio.mp3" }, 47 | { id: "myXmlFile", src: "assets/xml.xml" }, 48 | { id: "mySvgFile", src: "assets/xml.svg" }, 49 | { id: "myHtmlFile", src: "assets/xml.html" }, 50 | { id: "myDefaultXmlFile", src: "assets/xml", loader: "Xml" }, 51 | { id: "myFont", src: `assets/font.ttf` }, 52 | { id: "Space Regular", loader: "Font", fontOptions: { timeout: 10000 } }, 53 | // Can be retrieved with the src property eg. AsyncPreloader.items.get("assets/fileWithoutId") 54 | { src: "assets/fileWithoutId" }, 55 | ]; 56 | 57 | // Pass an array of LoadItem 58 | // 59 | // Returns a Promise with an array of LoadedValue 60 | const pItems = AsyncPreloader.loadItems(items); 61 | 62 | pItems 63 | .then((items) => { 64 | const element = AsyncPreloader.items.get("myVideoFile"); 65 | document.body.appendChild(element); 66 | }) 67 | .catch((error) => console.error("Error loading items", error)); 68 | ``` 69 | 70 | --- 71 | 72 | Note: Font loader will try to detect the font in the page using [FontFaceObserver](https://github.com/dmnsgn/fontfaceobserver) when no src is specified. 73 | 74 | ### Load items from a manifest file 75 | 76 | It works in a similar fashion as createjs's [PreloadJS](http://www.createjs.com/docs/preloadjs/classes/LoadQueue.html). 77 | 78 | ```javascript 79 | import AsyncPreloader from "async-preloader"; 80 | 81 | // Pass the file url and an optional path of the property to get in the JSON file. 82 | // It will load the file using the Json loader and look for the path key expecting an array of `LoadItem`s. 83 | // Default path is "items" eg the default manifest would look like this: 84 | // `{ "items": [ { "src": "assets/file1" }, { "src": "assets/file2" }] }` 85 | // 86 | // Returns a Promise with an array of LoadedValue 87 | const pItems = AsyncPreloader.loadManifest( 88 | "assets/manifest.json", 89 | "data.preloader.items" 90 | ); 91 | 92 | pItems 93 | .then((items) => useLoadedItemsFromManifest(items)) // or AsyncPreloader.items.get("src or id") 94 | .catch((error) => console.error("Error loading items", error)); 95 | ``` 96 | 97 | ## Advanced usage 98 | 99 | This section takes a closer look at the options of `AsyncPreloader`. 100 | 101 | ### Load a single item by using the [loaders](https://github.com/dmnsgn/async-preloader/blob/main/src/index.ts#L60) directly 102 | 103 | ```javascript 104 | import AsyncPreloader from "async-preloader"; 105 | 106 | // Pass a LoadItem 107 | // 108 | // Returns a Promise with the LoadedValue 109 | const pItem = AsyncPreloader.loadJson({ src: "assets/json.json" }); 110 | 111 | pItem 112 | .then((item) => useLoadedItem(item)) 113 | .catch((error) => console.error("Error loading item", error)); 114 | ``` 115 | 116 | --- 117 | 118 | Note: Using the loaders directly won't add the item to the `items` Map. 119 | Alternatively you could use `AsyncPreloader.loadItem` and rely on the file extension or add `{ loader: "Json"}` to the item. 120 | 121 | ### Load a single item by using a string directly 122 | 123 | ```javascript 124 | import AsyncPreloader from "async-preloader"; 125 | 126 | try { 127 | // Pass a string 128 | // 129 | // Returns a Promise with the LoadedValue 130 | const pItem = await AsyncPreloader.loadItem("assets/json.json"); 131 | } catch (error) { 132 | console.error(error); 133 | } 134 | ``` 135 | 136 | ### Get an `ArrayBuffer` instead of the default `Blob` 137 | 138 | You can specify how the response is handle by using the `body` key in a `LoadItem`. 139 | 140 | Typical use case: get an ArrayBuffer for the WebAudio API to decode the data with `baseAudioContext.decodeAudioData()`. 141 | 142 | ```javascript 143 | import AsyncPreloader from "async-preloader"; 144 | 145 | const audioContext = new AudioContext(); 146 | const pItem = AsyncPreloader.loadAudio({ 147 | src: "assets/audio.mp3", 148 | body: "arrayBuffer", 149 | }); 150 | 151 | pItem 152 | .then((item) => audioContext.decodeAudioData(item)) 153 | .then((decodedData) => useDecodedData(decodedData)) 154 | .catch((error) => console.error("Error decoding audio", error)); 155 | ``` 156 | 157 | ### Getting the progress 158 | 159 | Since `fetch` doesn't support `Progress events` yet, you might want to get a per file progress. 160 | 161 | ```javascript 162 | import AsyncPreloader from "async-preloader"; 163 | 164 | const items = [ 165 | { id: "myDefaultFile", src: "assets/default" }, // ... 166 | ]; 167 | 168 | let loadedCount = 0; 169 | 170 | async function preload() { 171 | await Promise.all( 172 | items.map(async (item) => { 173 | const data = await AsyncPreloader.loadItem(item); 174 | loadedCount++; 175 | console.log(`Progress: ${(100 * loadedCount) / items.length}%`); 176 | }) 177 | ); 178 | } 179 | 180 | await preload(); 181 | ``` 182 | 183 | ### Abort one or more loadItem(s) request(s) 184 | 185 | To abort a loadItem(s) call, you can create an `AbortController` instance and pass its signal to options. 186 | 187 | ```javascript 188 | const controller = new AbortController(); 189 | 190 | const timeoutId = setTimeout(() => { 191 | controller.abort(); 192 | }, 150); 193 | 194 | try { 195 | await AsyncPreloader.loadItems( 196 | items.map((item) => ({ 197 | ...item, 198 | options: { ...(item.options || {}), signal: controller.signal }, 199 | })) 200 | ); 201 | } catch (error) { 202 | if (error.name === "AbortError") console.log("Request was aborted"); 203 | } finally { 204 | clearTimeout(timeoutId); 205 | } 206 | ``` 207 | 208 | ## License 209 | 210 | MIT © [Damien Seguin](https://github.com/dmnsgn) 211 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/async-preloader/c1a85109afec2a2500fc29044601caef31825333/docs/.nojekyll -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-hl-4: #AF00DB; 11 | --dark-hl-4: #C586C0; 12 | --light-hl-5: #001080; 13 | --dark-hl-5: #9CDCFE; 14 | --light-hl-6: #0070C1; 15 | --dark-hl-6: #4FC1FF; 16 | --light-hl-7: #098658; 17 | --dark-hl-7: #B5CEA8; 18 | --light-hl-8: #008000; 19 | --dark-hl-8: #6A9955; 20 | --light-hl-9: #267F99; 21 | --dark-hl-9: #4EC9B0; 22 | --light-hl-10: #000000FF; 23 | --dark-hl-10: #D4D4D4; 24 | --light-code-background: #FFFFFF; 25 | --dark-code-background: #1E1E1E; 26 | } 27 | 28 | @media (prefers-color-scheme: light) { :root { 29 | --hl-0: var(--light-hl-0); 30 | --hl-1: var(--light-hl-1); 31 | --hl-2: var(--light-hl-2); 32 | --hl-3: var(--light-hl-3); 33 | --hl-4: var(--light-hl-4); 34 | --hl-5: var(--light-hl-5); 35 | --hl-6: var(--light-hl-6); 36 | --hl-7: var(--light-hl-7); 37 | --hl-8: var(--light-hl-8); 38 | --hl-9: var(--light-hl-9); 39 | --hl-10: var(--light-hl-10); 40 | --code-background: var(--light-code-background); 41 | } } 42 | 43 | @media (prefers-color-scheme: dark) { :root { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --hl-5: var(--dark-hl-5); 50 | --hl-6: var(--dark-hl-6); 51 | --hl-7: var(--dark-hl-7); 52 | --hl-8: var(--dark-hl-8); 53 | --hl-9: var(--dark-hl-9); 54 | --hl-10: var(--dark-hl-10); 55 | --code-background: var(--dark-code-background); 56 | } } 57 | 58 | :root[data-theme='light'] { 59 | --hl-0: var(--light-hl-0); 60 | --hl-1: var(--light-hl-1); 61 | --hl-2: var(--light-hl-2); 62 | --hl-3: var(--light-hl-3); 63 | --hl-4: var(--light-hl-4); 64 | --hl-5: var(--light-hl-5); 65 | --hl-6: var(--light-hl-6); 66 | --hl-7: var(--light-hl-7); 67 | --hl-8: var(--light-hl-8); 68 | --hl-9: var(--light-hl-9); 69 | --hl-10: var(--light-hl-10); 70 | --code-background: var(--light-code-background); 71 | } 72 | 73 | :root[data-theme='dark'] { 74 | --hl-0: var(--dark-hl-0); 75 | --hl-1: var(--dark-hl-1); 76 | --hl-2: var(--dark-hl-2); 77 | --hl-3: var(--dark-hl-3); 78 | --hl-4: var(--dark-hl-4); 79 | --hl-5: var(--dark-hl-5); 80 | --hl-6: var(--dark-hl-6); 81 | --hl-7: var(--dark-hl-7); 82 | --hl-8: var(--dark-hl-8); 83 | --hl-9: var(--dark-hl-9); 84 | --hl-10: var(--dark-hl-10); 85 | --code-background: var(--dark-code-background); 86 | } 87 | 88 | .hl-0 { color: var(--hl-0); } 89 | .hl-1 { color: var(--hl-1); } 90 | .hl-2 { color: var(--hl-2); } 91 | .hl-3 { color: var(--hl-3); } 92 | .hl-4 { color: var(--hl-4); } 93 | .hl-5 { color: var(--hl-5); } 94 | .hl-6 { color: var(--hl-6); } 95 | .hl-7 { color: var(--hl-7); } 96 | .hl-8 { color: var(--hl-8); } 97 | .hl-9 { color: var(--hl-9); } 98 | .hl-10 { color: var(--hl-10); } 99 | pre, code { background: var(--code-background); } 100 | -------------------------------------------------------------------------------- /docs/assets/icons.js: -------------------------------------------------------------------------------- 1 | (function(svg) { 2 | svg.innerHTML = ``; 3 | svg.style.display = 'none'; 4 | if (location.protocol === 'file:') { 5 | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', updateUseElements); 6 | else updateUseElements() 7 | function updateUseElements() { 8 | document.querySelectorAll('use').forEach(el => { 9 | if (el.getAttribute('href').includes('#icon-')) { 10 | el.setAttribute('href', el.getAttribute('href').replace(/.*#/, '#')); 11 | } 12 | }); 13 | } 14 | } 15 | })(document.body.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))) -------------------------------------------------------------------------------- /docs/assets/icons.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/material-style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 2 | 3 | :root, 4 | :root[data-theme="light"], 5 | :root[data-theme="dark"] { 6 | --font-sans: "Space Grotesk", sans-serif; 7 | --font-mono: "Space Mono", monospace; 8 | 9 | --color-background: var(--md-sys-color-surface-container); 10 | --color-background-secondary: var(--md-sys-color-surface-container-high); 11 | --color-background-warning: var(--md-sys-color-error-container); 12 | --color-warning-text: var(--md-sys-color-on-error-container); 13 | --color-icon-background: var(--md-sys-color-on-primary); 14 | --color-accent: var(--md-sys-color-secondary-container); 15 | --color-active-menu-item: var(--md-sys-color-surface-container-highest); 16 | --color-text: var(--md-sys-color-on-surface); 17 | --color-text-aside: var(--md-sys-color-on-surface-variant); 18 | --color-link: var(--md-sys-color-primary); 19 | 20 | --color-ts-project: var(--md-sys-color-secondary); 21 | --color-ts-module: var(--color-ts-project); 22 | --color-ts-namespace: var(--color-ts-project); 23 | 24 | --color-ts-enum: var(--md-sys-color-tertiary); 25 | --color-ts-enum-member: var(--color-ts-enum); 26 | 27 | --color-ts-variable: var(--md-sys-color-primary); 28 | --color-ts-function: var(--md-sys-color-secondary); 29 | --color-ts-class: var(--md-sys-color-tertiary); 30 | --color-ts-interface: var(--md-sys-color-tertiary); 31 | 32 | --color-ts-constructor: var(--md-sys-color-inverse-primary); 33 | 34 | --color-ts-property: var(--md-sys-color-on-background); 35 | --color-ts-method: var(--color-ts-function); 36 | 37 | --color-ts-call-signature: var(--color-ts-method); 38 | --color-ts-index-signature: var(--color-ts-property); /* ? */ 39 | --color-ts-constructor-signature: var(--color-ts-function); 40 | --color-ts-parameter: var(--md-sys-color-primary); 41 | 42 | --color-ts-type-parameter: var(--md-sys-color-tertiary); 43 | --color-ts-accessor: var(--color-ts-property); 44 | --color-ts-get-signature: var(--color-ts-accessor); 45 | --color-ts-set-signature: var(--color-ts-accessor); 46 | --color-ts-type-alias: var(--md-sys-color-tertiary); 47 | 48 | /* --external-icon: var(--md-sys-external-icon); 49 | --color-scheme: var(--md-sys-color-scheme); */ 50 | 51 | --top-app-bar-height: 4.5rem; 52 | --footer-height: 3.5rem; 53 | } 54 | 55 | body { 56 | font-family: var(--font-sans); 57 | } 58 | code, 59 | pre { 60 | font-family: var(--font-mono); 61 | } 62 | 63 | img { 64 | max-width: 100%; 65 | } 66 | 67 | *::-webkit-scrollbar { 68 | width: 8px; 69 | height: 8px; 70 | } 71 | *::-webkit-scrollbar-track { 72 | background: none; 73 | } 74 | *::-webkit-scrollbar-thumb { 75 | border: none; 76 | } 77 | 78 | .container-main { 79 | min-height: calc(100vh - var(--top-app-bar-height) - var(--footer-height)); 80 | } 81 | .col-content { 82 | overflow: hidden; 83 | box-sizing: border-box; 84 | padding: 1.75rem; 85 | border-radius: 28px; 86 | background-color: var(--md-sys-color-surface); 87 | } 88 | .page-menu { 89 | height: fit-content; 90 | padding: 0.75rem 1.75rem; 91 | border-radius: 28px; 92 | background-color: var(--md-sys-color-surface); 93 | } 94 | .site-menu > *, 95 | .page-menu > * { 96 | position: relative; 97 | } 98 | .title { 99 | display: block; 100 | max-width: calc(100% - 5rem); 101 | white-space: nowrap; 102 | overflow: hidden; 103 | text-overflow: ellipsis; 104 | font-size: 22px; 105 | } 106 | 107 | .tsd-page-toolbar { 108 | padding: 8px 0; 109 | height: calc(var(--top-app-bar-height) - 16px); 110 | background-color: var(--color-background); 111 | border-bottom: none; 112 | } 113 | .tsd-page-toolbar .tsd-toolbar-contents { 114 | height: 56px; 115 | } 116 | .tsd-page-toolbar .table-cell { 117 | height: 56px; 118 | margin-left: 1.5rem; 119 | } 120 | .tsd-page-toolbar .tsd-toolbar-icon { 121 | padding: 20px 0; 122 | } 123 | #tsd-search { 124 | line-height: 56px; 125 | border-radius: 22px; 126 | } 127 | #tsd-search .results { 128 | z-index: -1; 129 | top: calc(56px - 22px); 130 | padding-top: 22px; 131 | box-shadow: 0px 4px 2px rgba(0, 0, 0, 0.125); 132 | background-color: var(--color-background-secondary); 133 | border-bottom-left-radius: 22px; 134 | border-bottom-right-radius: 22px; 135 | overflow: hidden; 136 | } 137 | #tsd-search .results li { 138 | background: none; 139 | } 140 | #tsd-search .results a { 141 | padding: 1rem 0.25rem; 142 | } 143 | .col-sidebar { 144 | padding-top: 0; 145 | margin-right: 1rem; 146 | } 147 | 148 | .tsd-signature { 149 | padding: 1rem 1.5rem; 150 | border-radius: 24px; 151 | background-color: var(--md-sys-color-surface-container); 152 | } 153 | 154 | .tsd-page-navigation ul { 155 | padding-left: 0.44rem; 156 | } 157 | .tsd-navigation a, 158 | .tsd-navigation summary > span, 159 | .tsd-page-navigation a { 160 | padding: 0.88rem; 161 | border-radius: 24px; 162 | } 163 | .tsd-navigation a:hover, 164 | .tsd-page-navigation a:hover { 165 | text-decoration: none; 166 | background-color: var(--md-sys-color-surface-container-high); 167 | } 168 | .page-menu .tsd-accordion-summary svg { 169 | position: absolute; 170 | right: 0; 171 | } 172 | .site-menu .tsd-navigation .tsd-accordion-summary { 173 | display: flex; 174 | flex-direction: row-reverse; 175 | width: 100%; 176 | } 177 | 178 | .tsd-small-nested-navigation { 179 | margin-left: 1rem; 180 | } 181 | .tsd-nested-navigation { 182 | margin-left: 2.5rem; 183 | } 184 | .tsd-nested-navigation > li > a, 185 | .tsd-nested-navigation > li > span { 186 | width: 100%; 187 | } 188 | .tsd-navigation > a, 189 | .tsd-navigation .tsd-accordion-summary { 190 | width: 100%; 191 | } 192 | .tsd-index-accordion .tsd-accordion-summary > svg { 193 | position: absolute; 194 | right: 1.5rem; 195 | margin-top: 1rem; 196 | } 197 | .tsd-accordion-summary .tsd-kind-icon ~ span { 198 | margin-right: 2.5rem; 199 | } 200 | .tsd-accordion-summary .tsd-nested-navigation > li > a, 201 | .tsd-nested-navigation > li > span { 202 | width: calc(100% - 0.44rem); 203 | } 204 | .tsd-kind-icon ~ span { 205 | white-space: nowrap; 206 | overflow: hidden; 207 | text-overflow: ellipsis; 208 | } 209 | .tsd-generator { 210 | padding: 0; 211 | border-top: none; 212 | height: var(--footer-height); 213 | line-height: var(--footer-height); 214 | } 215 | .tsd-generator > p { 216 | padding: 0 2rem; 217 | } 218 | 219 | @media (max-width: 769px) { 220 | .container { 221 | padding: 1rem; 222 | } 223 | .col-sidebar { 224 | margin-right: 0; 225 | } 226 | } 227 | @media (min-width: 770px) { 228 | .container-main { 229 | margin: 0 auto; 230 | } 231 | .site-menu { 232 | margin-right: 0.5rem; 233 | } 234 | } 235 | @media (min-width: 1200px) { 236 | .page-menu, 237 | .site-menu { 238 | max-height: calc(100vh - var(--footer-height) - var(--top-app-bar-height)); 239 | top: var(--top-app-bar-height); 240 | } 241 | .page-menu { 242 | margin-left: 1rem; 243 | } 244 | .col-sidebar { 245 | margin-right: 0; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAE43PzUrEMBQF4He566JOZfzpzlkI4gyKCxGGLmJzhwbTpCS3wxTpu4uhTdIq7WxzD1/O2X8D4YkgA6E4niCBmlEJGVSaNxLtpXu+KKmSkMCXUByyNIGiFJIbVJDtPfBgW1W8GpSacTRBKiSz1kvj1BhepXdd4j2OB9ZICtCRGcE+Q6k+MDau0y6PEGprtH9XueezVm1dz2dsA4KqqQbCn8dYvONRK3qpSWgVFRGK0BxY4btEqUmv9U2E/f73RFjNSENkiUHzzmSDC1KfmsM2mrc7pFLzYDmgZ8J9olzd367W6UR6Q6vlESfdpl6cWlLdDD4DRoHzrI/ddpEbMv+LeZf/ACa24ZB7AwAA" -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = "data:application/octet-stream;base64,H4sIAAAAAAAAE7Wa227bRhCG32V9y7reEy3prkEbIG3SBm0RBBCMgBZXDluRFEjKtWH43YslaXGGml1xLfnS1vz7D+fbw/DwxKryv5otlk/s36xI2UJErEhywxYsK1LzwCK2qzZswfIy3W1M/WP738vvTb5hEVttkro2NVsw9hy9jMDFbD/GT/VjsfpcmU2ZpKbaD9br+sFwEDF2xLZJZYpmnxQwuxJq79YNUE+3uRgUg11Tpz9k9Q/bKrtPGnNgPhrHlUta5p+Tqg656AuoOUM+4koNJNamWX3/0Jg8IB+oOXc+d6b5XJXbgGwGxRvk8j7bmF8eGlPUWVmEJTWWvlF275La/G7/E5wcUL5Rbq/L6+1y+tgG/GYew5KCsjfI6lOWm78ft4GVAqoz5KT5sMGvyqJuqt2qKUM2Kazy7NITN8qsMXnIlv0Sf7pzatbJbtO8K9PHT6b5XqYhWzWhPVtGHwNPy4ux7lWZoOlqf/0QSAZqzpfBKxI4m/+npMjWpm4CcwCy8+Txt3kIzaGXnMf/1zroUASS8/j/VFXJ47vdeh20Kg6V58nm3aa8DUyjl5zH/31Z5T8nTRKYA5CdaXXmyV3IYQY158ngS5aaMjCDF82ZZuYuzUIzeNGcJ4Ov7d1SiP/XY/dXQXOxCN2Zesmr/KUYn5V78/ukypLb4Ra1/z3sdnIYvnncmvrg7rf9r/fuV1zNr7kexiG6i3aQfqjhZ3+iXTp+mz9NXW7uzZdkszNOMxh0mmXbbaRfP310G+KQc9gd83qNkY6RCWo6sqIx1TpZIRsbEeaBury6Wk0c/6IL9Xjs453tdTrVK3P3sNOsRg95jtjto0+xLLdNVhb1VM8h/BTTdVk0f4QZY8kp5rdl+jjVtY89xS4f37Mescypu9Vw26L82azKdLItiA+0neENBj00MMUuR9uL/TVw4Q97C2qi3UNfeDvnkYJ2otplj+GUHnmSL2qMPYbebniS00EL7HE72vdOckS3YB43733XJCfcVnus/L30JC/cQHu8/F3ztFmJWmXffPT2x5O8YFPscfJ1whPnYTFpVnh7XrcTbE3eTzl1QNAJDUpq6lWVbZuyCrC6wCrfpcJLcaTQdvOgtsftB8WJ1o2pm7+aKivuAtyR6NQEstyUu5BrHxTh1uP211S4xyZP3j7ohDlmXt6YHOmhoNMFEh1fTN2FnNTZIPuA5ubQPNZaDpX+9q15nfflXjkxhct91v6HzgdvJ47ndKicXJabqL/3Xjyxe1O1r84WTFzKyzmL2Dozm9S+le5yjNiqzHM71k3/2xezaveZxbIL+fGKRcurSF5fxrP5zU20fFG0P7T/aMM4i5acCuMoTLBoKagwgcIki5aSCpMoTLFoqagwhcI0i5aaCtMoLGbRMqbCYhR2zaLlNRV2jcJmLFrOqLAZCpuzaDmnwua4vLbanOTARyBaEjQKzILbmnOSBsc4uC07J4FwTITbynOSCcdQuC0+J7FwzIXb+vM4kuJSzziOxGi4RcBJOBzT4ZYCJ/lwDIhbEJxExDEjYUEIkpHAjIQFIUhGYrRe2gVDrxjMSFgQgmQkMCNhQQiSkcCMhAUhSEYCMxIWhCBXj8CMhAUhSEYCMxIWhCAZCcxIWBCCZCQwI2lBSJKRxIykBSFJRhIzkhaEJBnJ0bbW7mv0xoYZSeWc8xIzkhaEJGlKzEhaEJKkKTEjaUFIkqbEjKQFIUmaEjOSFoSckVeEGamWEUlTYUbKglBX1JgKM1IWhCJpKsxIWRCKpKlGp097/JA0FWakLAhFn1SYkbIgFMlIYUbKglAkI4UZKQtCkYwUZqQsCEWuOIUZaQtCkYw0ZqQtCE2uOI0ZaQtCk4w0ZqQtCE0y0piRVs5Zp0dNQtslkDQ1ZqQtCE3S1JiRtiA03XpgRtqC0CRNjRlpC0KTNDVmFFsQmqQZY0axex3FmFHcMiK5x5hRbEHEJPe4Y9R2qvemakz6oetYl8t9+/7EvvVtrJ6/NNRPLBZs8fT8PLSt9i87dGKftN32T9oGrVKDVmmX1r4T2w6fUg5yPqi5S9w9UgHpXg0i7VLdts/pQJ4a5Bm7RO1D50EkQWHUlUeU92/JgBQkKX3Sqnupdd/duIIBQGmk6yrRB02gqrCsLu3+JeSgG75+fWJi5tfRl80lcJb+EYjZACYTV361vTUcT+QYFD12u4NnPWBWgcy1ayKnZb7tv3IFpAAohw7e/ANLUO7YNUfaD1mz9r0emM1gXrp07WM7YCbA9bnytKL9iyZwhSBRObfREdNu4ypP22fWIN8YJHztEN6ZZp1tzG1Sm+7WedBfD/IjajN8zQrmBaizXz42BhfuWg93pp/J/xq0eYC9Y+6WUtOYg2nMXfPizjTb9pNigBgQdqgyvEkBhXRVJuse1QOYoCjKdWn9F/iDClyT65L6zyNBIcCc5a6D6Z8aw1ZgPSrXJLXEHCeaAJkKV6qtfnwwCVBO4SJglePDSYD9Wrj263aSpYfnBNx+XOu60z7km0M5KLF0lfhwp5ZgSUnXkiSXhQKGypsv3p+BzJ/mwSVqsHto13y12vGWKYBQ+C6S2vIETNiX8cH6EoCo8FVofCpwAIXH3TYtfSvgcMGBGcx9M3i86DjY7bivxvn++1IgBnXmvjo37QtKIAQ7EXftz1Z4370CBDUGJ6jwlejBviQAOlBi4doxqT1dgV1F8Y5N7FrqRZn2nwSAAcAeoVzziTq7QXGlq0bttzpAA6ojXdPAvjaq+9dGYLHBQ8jFcsxRgRyVK8f9iyJgBmhop677Gg4wBHNVuObq/pUccANV0S72BzNNATftOk5Gs0wD1ppkfROxbbY1m6wwbLG8eX7+H02euE4NNwAA"; -------------------------------------------------------------------------------- /docs/modules/index.html: -------------------------------------------------------------------------------- 1 | index | async-preloader

Index

Classes

Variables

default 161 |
-------------------------------------------------------------------------------- /docs/modules/types.html: -------------------------------------------------------------------------------- 1 | types | async-preloader

Index

Enumerations

LoaderKey 160 |

Interfaces

FontOptions 161 | LoadItem 162 | LoaderValue 163 |

Type Aliases

-------------------------------------------------------------------------------- /docs/types/types.BodyMethod.html: -------------------------------------------------------------------------------- 1 | BodyMethod | async-preloader

Type alias BodyMethod

BodyMethod: "arrayBuffer" | "blob" | "formData" | "json" | "text"

Methods that can be called on a Request (object returned by fetch and that implements the Body interface)

160 |
-------------------------------------------------------------------------------- /docs/types/types.BodyResolveValue.html: -------------------------------------------------------------------------------- 1 | BodyResolveValue | async-preloader

Type alias BodyResolveValue

BodyResolveValue: ArrayBuffer | Blob | FormData | JSON | string

Types that can be returned by all the BodyMethod

160 |
-------------------------------------------------------------------------------- /docs/types/types.LoadedValue.html: -------------------------------------------------------------------------------- 1 | LoadedValue | async-preloader

Type alias LoadedValue

LoadedValue: BodyResolveValue | HTMLImageElement | HTMLVideoElement | HTMLAudioElement | LoadedXMLValue

Types that can be returned by all the loaders.

160 |
-------------------------------------------------------------------------------- /docs/types/types.LoadedXMLValue.html: -------------------------------------------------------------------------------- 1 | LoadedXMLValue | async-preloader

Type alias LoadedXMLValue

LoadedXMLValue: Document | XMLDocument

Types that can be returned by the Xml loader. See the LoadItem.mimeType.

160 |
-------------------------------------------------------------------------------- /docs/variables/index.default.html: -------------------------------------------------------------------------------- 1 | default | async-preloader

Variable defaultConst

default: AsyncPreloader = ...
-------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | async-preloader by Damien Seguin 9 | (https://github.com/dmnsgn/async-preloader) 10 | 11 | 30 | 31 | 32 |
33 |

async-preloader

34 |
35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-preloader", 3 | "version": "8.0.4", 4 | "description": "Assets preloader using async/await and fetch for usage both in the browser and Node.js.", 5 | "keywords": [ 6 | "preloader", 7 | "assets", 8 | "async", 9 | "await", 10 | "typescript", 11 | "ES2017", 12 | "fetch" 13 | ], 14 | "homepage": "https://github.com/dmnsgn/async-preloader", 15 | "bugs": "https://github.com/dmnsgn/async-preloader/issues", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/dmnsgn/async-preloader.git" 19 | }, 20 | "funding": [ 21 | { 22 | "type": "individual", 23 | "url": "https://paypal.me/dmnsgn" 24 | }, 25 | { 26 | "type": "individual", 27 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3" 28 | } 29 | ], 30 | "license": "MIT", 31 | "author": "Damien Seguin (https://github.com/dmnsgn)", 32 | "type": "module", 33 | "exports": { 34 | ".": { 35 | "types": "./types/index.d.ts", 36 | "default": "./lib/index.js" 37 | } 38 | }, 39 | "main": "lib/index.js", 40 | "types": "types/index.d.ts", 41 | "scripts": { 42 | "coverage": "nyc report --reporter=text --reporter=html", 43 | "dev": "npx snowdev --no-serve", 44 | "prejest": "npx snowdev install", 45 | "jest": "NODE_OPTIONS=--experimental-vm-modules npx jest test/ --watch --no-cache", 46 | "pretest": "npx snowdev install && npx snowdev build --no-docs --no-lint", 47 | "test": "NODE_OPTIONS=--experimental-vm-modules npx jest test/ --runInBand" 48 | }, 49 | "jest": { 50 | "globalSetup": "jest-environment-puppeteer/setup", 51 | "globalTeardown": "/test/global-teardown.js", 52 | "moduleNameMapper": { 53 | "^(\\.{1,2}/.*)\\.js$": "$1" 54 | }, 55 | "preset": "ts-jest/presets/js-with-ts-esm", 56 | "resolver": "jest-ts-webcompat-resolver", 57 | "setupFilesAfterEnv": [ 58 | "expect-puppeteer" 59 | ], 60 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|js)x?$", 61 | "transform": { 62 | "^.+\\.(t|j)s$": [ 63 | "ts-jest", 64 | { 65 | "useESM": true 66 | } 67 | ] 68 | } 69 | }, 70 | "dependencies": { 71 | "fontfaceobserver-es": "^3.3.3", 72 | "tslib": "^2.6.3" 73 | }, 74 | "devDependencies": { 75 | "@types/css-font-loading-module": "^0.0.13", 76 | "@types/expect-puppeteer": "^5.0.6", 77 | "@types/jest": "^29.5.12", 78 | "@types/jest-environment-puppeteer": "^5.0.6", 79 | "@types/node": "^20.14.10", 80 | "@types/puppeteer": "^5.4.7", 81 | "@xmldom/xmldom": "^0.8.10", 82 | "es-module-shims": "^1.10.0", 83 | "jest": "^29.7.0", 84 | "jest-puppeteer": "^10.0.1", 85 | "jest-ts-webcompat-resolver": "^1.0.0", 86 | "mime-types": "^2.1.35", 87 | "nyc": "^17.0.0", 88 | "portfinder": "^1.0.32", 89 | "puppeteer": "^22.12.1", 90 | "puppeteer-to-istanbul": "^1.4.0", 91 | "snowdev": "^2.2.3", 92 | "ts-jest": "^29.1.5", 93 | "typescript": "^5.5.3" 94 | }, 95 | "engines": { 96 | "node": ">=22.0.0", 97 | "npm": ">=10.5.1", 98 | "snowdev": ">=2.2.x" 99 | }, 100 | "jest-puppeteer": { 101 | "launch": { 102 | "headless": "new" 103 | } 104 | }, 105 | "snowdev": { 106 | "dependencies": [ 107 | "es-module-shims", 108 | "fontfaceobserver-es" 109 | ] 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import FontFaceObserver from "fontfaceobserver-es"; 2 | 3 | import { 4 | BodyMethod, 5 | LoadItem, 6 | LoadedValue, 7 | LoadedXMLValue, 8 | LoaderKey, 9 | LoaderValue, 10 | FontOptions, 11 | } from "./types.js"; 12 | 13 | const isSafari = 14 | /^((?!chrome|android).)*safari/i.test(globalThis.navigator?.userAgent) === 15 | true; 16 | 17 | /** 18 | * AsyncPreloader: assets preloader using ES2017 async/await and fetch. 19 | * 20 | * It exports an instance of itself as default so you can: 21 | * 22 | * ```js 23 | * import Preloader from "async-preloader"; 24 | * 25 | * await Preloader.loadItems([]); 26 | * ``` 27 | * 28 | * to use directly as a singleton or 29 | * 30 | * ```js 31 | * import { AsyncPreloader as Preloader } from "async-preloader"; 32 | * 33 | * const preloader = new Preloader(); 34 | * await preloader.loadItems([]); 35 | * ``` 36 | * if you need more than one instance. 37 | */ 38 | class AsyncPreloader { 39 | // Properties 40 | /** 41 | * Object that contains the loaded items 42 | */ 43 | public items: Map = new Map(); 44 | 45 | /** 46 | * Default body method to be called on the Response from fetch if no body option is specified on the LoadItem 47 | */ 48 | public defaultBodyMethod: BodyMethod = "blob"; 49 | 50 | /** 51 | * Default loader to use if no loader key is specified in the {@link LoadItem} or if the extension doesn't match any of the {@link AsyncPreloader.loaders} extensions 52 | */ 53 | public defaultLoader: LoaderKey = LoaderKey.Text; 54 | 55 | /** 56 | * Loader types and the extensions they handle 57 | * 58 | * Allows the omission of the loader key in a {@link LoadItem.loader} for some generic extensions 59 | */ 60 | private static loaders: Map = new Map() 61 | .set(LoaderKey.Text, { extensions: ["txt"] }) 62 | .set(LoaderKey.Json, { extensions: ["json"] }) 63 | .set(LoaderKey.Image, { extensions: ["jpeg", "jpg", "gif", "png", "webp"] }) 64 | .set(LoaderKey.Video, { extensions: ["webm", "ogg", "mp4"] }) 65 | .set(LoaderKey.Audio, { extensions: ["webm", "ogg", "mp3", "wav", "flac"] }) 66 | .set(LoaderKey.Xml, { 67 | extensions: ["xml", "svg", "html"], 68 | mimeType: { 69 | xml: "text/xml", 70 | svg: "image/svg+xml", 71 | html: "text/html", 72 | }, 73 | defaultMimeType: "text/xml", 74 | }) 75 | .set(LoaderKey.Font, { 76 | extensions: ["woff2", "woff", "ttf", "otf", "eot"], 77 | }); 78 | 79 | /** 80 | * DOMParser instance for the XML loader 81 | */ 82 | private static domParser = 83 | typeof DOMParser !== "undefined" && new DOMParser(); 84 | 85 | // API 86 | /** 87 | * Load the specified manifest (array of items) 88 | * 89 | * @param items Items to load 90 | * @returns Resolve when all items are loaded, reject for any error 91 | */ 92 | public loadItems = async ( 93 | items: LoadItem[] | string[], 94 | ): Promise => { 95 | return await Promise.all(items.map(this.loadItem)); 96 | }; 97 | 98 | /** 99 | * Load a single item 100 | * 101 | * @param item Item to load 102 | * @returns Resolve when item is loaded, reject for any error 103 | */ 104 | public loadItem = async (item: LoadItem | string): Promise => { 105 | if (typeof item === "string") item = { src: item }; 106 | 107 | const extension: string = AsyncPreloader.getFileExtension( 108 | (item.src as string) || "", 109 | ); 110 | const loaderKey: LoaderKey = 111 | item.loader || AsyncPreloader.getLoaderKey(extension); 112 | 113 | const loadedItem: LoadedValue = await this[`load` + loaderKey](item); 114 | 115 | this.items.set((item.id || item.src) as string, loadedItem); 116 | 117 | return loadedItem; 118 | }; 119 | 120 | // Special loaders 121 | /** 122 | * Load a manifest of items 123 | * 124 | * @param src Manifest src url 125 | * @param key Manifest key in the JSON object containing the array of LoadItem. 126 | * @returns 127 | */ 128 | public loadManifest = async ( 129 | src: string, 130 | key = "items", 131 | ): Promise => { 132 | const loadedManifest: JSON = await this.loadJson({ 133 | src, 134 | }); 135 | const items: LoadItem[] = AsyncPreloader.getProp(loadedManifest, key); 136 | 137 | return await this.loadItems(items); 138 | }; 139 | 140 | // Text loaders 141 | /** 142 | * Load an item and parse the Response as text 143 | * 144 | * @param item Item to load 145 | * @returns Fulfilled value of parsed Response 146 | */ 147 | public loadText = async (item: LoadItem): Promise => { 148 | const response: Response = await AsyncPreloader.fetchItem(item); 149 | return await response.text(); 150 | }; 151 | 152 | /** 153 | * Load an item and parse the Response as json 154 | * 155 | * @param item Item to load 156 | * @returns Fulfilled value of parsed Response 157 | */ 158 | public loadJson = async (item: LoadItem): Promise => { 159 | const response: Response = await AsyncPreloader.fetchItem(item); 160 | return await response.json(); 161 | }; 162 | 163 | /** 164 | * Load an item and parse the Response as arrayBuffer 165 | * 166 | * @param item Item to load 167 | * @returns Fulfilled value of parsed Response 168 | */ 169 | public loadArrayBuffer = async (item: LoadItem): Promise => { 170 | const response: Response = await AsyncPreloader.fetchItem(item); 171 | return await response.arrayBuffer(); 172 | }; 173 | 174 | /** 175 | * Load an item and parse the Response as blob 176 | * 177 | * @param item Item to load 178 | * @returns Fulfilled value of parsed Response 179 | */ 180 | public loadBlob = async (item: LoadItem): Promise => { 181 | const response: Response = await AsyncPreloader.fetchItem(item); 182 | return await response.blob(); 183 | }; 184 | 185 | /** 186 | * Load an item and parse the Response as formData 187 | * 188 | * @param item Item to load 189 | * @returns Fulfilled value of parsed Response 190 | */ 191 | public loadFormData = async (item: LoadItem): Promise => { 192 | const response: Response = await AsyncPreloader.fetchItem(item); 193 | return await response.formData(); 194 | }; 195 | 196 | // Custom loaders 197 | /** 198 | * Load an item in one of the following cases: 199 | * - item's "loader" option set as "Image" 200 | * - item's "src" option extensions matching the loaders Map 201 | * - direct call of the method 202 | * 203 | * @param item Item to load 204 | * @returns Fulfilled value with a decoded HTMLImageElement instance of or a parsed Response according to the "body" option. Defaults to a decoded HTMLImageElement. 205 | */ 206 | public loadImage = async (item: LoadItem): Promise => { 207 | const image = new Image(); 208 | 209 | if (item.body) { 210 | const response: Response = await AsyncPreloader.fetchItem(item); 211 | const data: LoadedValue = await response[item.body](); 212 | 213 | if (item.body !== "blob") return data; 214 | 215 | return await new Promise((resolve, reject) => { 216 | image.addEventListener("load", function load() { 217 | image.removeEventListener("load", load); 218 | resolve(image); 219 | }); 220 | image.addEventListener("error", function error(event) { 221 | image.removeEventListener("error", error); 222 | reject(event); 223 | }); 224 | image.src = URL.createObjectURL(data as Blob); 225 | }); 226 | } 227 | 228 | image.src = item.src as string; 229 | 230 | if (!item.noDecode) await image.decode(); 231 | 232 | return image; 233 | }; 234 | 235 | /** 236 | * Load an item in one of the following cases: 237 | * - item's "loader" option set as "Video" 238 | * - item's "src" option extensions matching the loaders Map 239 | * - direct call of the method 240 | * 241 | * @param item Item to load 242 | * @returns Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLVideoElement with a blob as srcObject or src. 243 | */ 244 | public loadVideo = async (item: LoadItem): Promise => { 245 | const response: Response = await AsyncPreloader.fetchItem(item); 246 | const data: LoadedValue = 247 | await response[item.body || this.defaultBodyMethod](); 248 | 249 | if (item.body) return data; 250 | 251 | const video = document.createElement("video"); 252 | 253 | return await new Promise((resolve, reject) => { 254 | video.addEventListener("canplaythrough", function canplaythrough() { 255 | video.removeEventListener("canplaythrough", canplaythrough); 256 | resolve(video); 257 | }); 258 | video.addEventListener("error", function error(event) { 259 | video.removeEventListener("error", error); 260 | reject(event); 261 | }); 262 | 263 | try { 264 | if (isSafari) throw ""; 265 | video.srcObject = data as Blob; 266 | } catch (error) { 267 | video.src = URL.createObjectURL(data as Blob); 268 | } 269 | 270 | video.load(); 271 | }); 272 | }; 273 | 274 | /** 275 | * Load an item in one of the following cases: 276 | * - item's "loader" option set as "Audio" 277 | * - item's "src" option extensions matching the loaders Map 278 | * - direct call of the method 279 | * 280 | * @param item Item to load 281 | * @returns Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLAudioElement with a blob as srcObject or src. 282 | */ 283 | public loadAudio = async (item: LoadItem): Promise => { 284 | const response: Response = await AsyncPreloader.fetchItem(item); 285 | const data: LoadedValue = 286 | await response[item.body || this.defaultBodyMethod](); 287 | 288 | if (item.body) return data; 289 | 290 | const audio = document.createElement("audio"); 291 | audio.autoplay = false; 292 | audio.preload = "auto"; 293 | 294 | return await new Promise((resolve, reject) => { 295 | audio.addEventListener("canplaythrough", function canplaythrough() { 296 | audio.removeEventListener("canplaythrough", canplaythrough); 297 | resolve(audio); 298 | }); 299 | audio.addEventListener("error", function error(event) { 300 | audio.removeEventListener("error", error); 301 | reject(event); 302 | }); 303 | 304 | try { 305 | if (isSafari) throw ""; 306 | audio.srcObject = data as Blob; 307 | } catch (error) { 308 | audio.src = URL.createObjectURL(data as Blob); 309 | } 310 | 311 | audio.load(); 312 | }); 313 | }; 314 | 315 | /** 316 | * Load an item in one of the following cases: 317 | * - item's "loader" option set as "Xml" 318 | * - item's "src" option extensions matching the loaders Map 319 | * - direct call of the method 320 | * 321 | * @param item Item to load (need a mimeType specified or default to "application/xml") 322 | * @returns Result of Response parsed as a document. 323 | */ 324 | public loadXml = async (item: LoadItem): Promise => { 325 | if (!item.mimeType) { 326 | const extension: string = AsyncPreloader.getFileExtension( 327 | item.src as string, 328 | ); 329 | item = { 330 | ...item, 331 | mimeType: AsyncPreloader.getMimeType(LoaderKey.Xml, extension), 332 | }; 333 | } 334 | 335 | if (!AsyncPreloader.domParser) { 336 | throw new Error("DomParser is not supported."); 337 | } 338 | 339 | const response: Response = await AsyncPreloader.fetchItem(item); 340 | const data: string = await response.text(); 341 | 342 | return AsyncPreloader.domParser.parseFromString(data, item.mimeType); 343 | }; 344 | 345 | /** 346 | * Load a font via FontFace or check a font is loaded via FontFaceObserver instance 347 | * 348 | * @param item Item to load (id correspond to the font family name). 349 | * @returns Fulfilled value with FontFace instance or initial id if no src provided. 350 | */ 351 | public loadFont = async (item: LoadItem): Promise => { 352 | const fontName = (item.id || 353 | AsyncPreloader.getFileName(item.src as string)) as string; 354 | const options = (item.fontOptions || {}) as FontOptions; 355 | 356 | if (!item.src) { 357 | const font = new FontFaceObserver( 358 | fontName, 359 | (options.variant as FontFaceObserver.FontVariant) || {}, 360 | ); 361 | await font.load(options.testString, options.timeout); 362 | 363 | return fontName; 364 | } 365 | 366 | const source = 367 | item.body === "arrayBuffer" 368 | ? await this.loadArrayBuffer({ src: item.src }) 369 | : `url(${item.src})`; 370 | 371 | const font = new FontFace(fontName, source, options.descriptors); 372 | 373 | return await font.load().then((font) => { 374 | document.fonts.add(font); 375 | return font; 376 | }); 377 | }; 378 | 379 | // Utils 380 | /** 381 | * Fetch wrapper for LoadItem 382 | * 383 | * @param item Item to fetch 384 | * @returns Fetch response 385 | */ 386 | private static fetchItem(item: LoadItem): Promise { 387 | return fetch(item.src, item.options || {}); 388 | } 389 | 390 | /** 391 | * Get an object property by its path in the form 'a[0].b.c' or ['a', '0', 'b', 'c']. 392 | * Similar to [lodash.get](https://lodash.com/docs/4.17.5#get). 393 | * 394 | * @param object Object with nested properties 395 | * @param path Path to the desired property 396 | * @returns The returned object property 397 | */ 398 | private static getProp(object: unknown, path: string | string[]) { 399 | const p = Array.isArray(path) 400 | ? path 401 | : path.split(".").filter((index) => index.length); 402 | 403 | if (!p.length) return object; 404 | 405 | return AsyncPreloader.getProp(object[p.shift()], p); 406 | } 407 | 408 | /** 409 | * Get file extension 410 | * 411 | * @param path 412 | * @returns 413 | */ 414 | private static getFileExtension(path: string): string { 415 | return (path?.match(/[^\\/]\.([^.\\/]+)$/) || [null]).pop(); 416 | } 417 | 418 | /** 419 | * Get file base name 420 | * 421 | * @param path 422 | * @returns 423 | */ 424 | private static getFileBaseName(path: string): string { 425 | return path.split(/[\\/]/).pop(); 426 | } 427 | 428 | /** 429 | * Get file name 430 | * 431 | * @param path 432 | * @returns 433 | */ 434 | private static getFileName(path: string): string { 435 | return ( 436 | AsyncPreloader.getFileBaseName(path).split(".").slice(0, -1).join(".") || 437 | path 438 | ); 439 | } 440 | 441 | /** 442 | * Retrieve loader key from extension (when the loader option isn't specified in the LoadItem) 443 | * 444 | * @param extension 445 | * @returns 446 | */ 447 | private static getLoaderKey(extension: string): LoaderKey { 448 | const loader = Array.from(AsyncPreloader.loaders).find((loader) => 449 | loader[1].extensions.includes(extension), 450 | ); 451 | return loader ? loader[0] : LoaderKey.Text; 452 | } 453 | 454 | /** 455 | * Retrieve mime type from extension 456 | * 457 | * @param loaderKey 458 | * @param extension 459 | * @returns 460 | */ 461 | private static getMimeType( 462 | loaderKey: LoaderKey, 463 | extension: string, 464 | ): DOMParserSupportedType { 465 | const loader: LoaderValue = AsyncPreloader.loaders.get(loaderKey); 466 | return loader.mimeType[extension] || loader.defaultMimeType; 467 | } 468 | } 469 | 470 | export { AsyncPreloader }; 471 | 472 | const AsyncPreloaderInstance = new AsyncPreloader(); 473 | export default AsyncPreloaderInstance; 474 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Methods that can be called on a Request (object returned by fetch and that implements the [Body](https://developer.mozilla.org/en-US/docs/Web/API/Response/body) interface) 3 | */ 4 | export type BodyMethod = "arrayBuffer" | "blob" | "formData" | "json" | "text"; 5 | 6 | /** 7 | * Types that can be returned by all the {@link BodyMethod} 8 | */ 9 | export type BodyResolveValue = ArrayBuffer | Blob | FormData | JSON | string; 10 | 11 | /** 12 | * Types that can be returned by the Xml loader. See the {@link LoadItem.mimeType}. 13 | */ 14 | export type LoadedXMLValue = Document | XMLDocument; 15 | 16 | /** 17 | * Types that can be returned by all the loaders. 18 | */ 19 | export type LoadedValue = 20 | | BodyResolveValue 21 | | HTMLImageElement 22 | | HTMLVideoElement 23 | | HTMLAudioElement 24 | | LoadedXMLValue; 25 | 26 | /** 27 | * Main interface representing an object to load. src is the only mandatory key. 28 | */ 29 | export interface LoadItem { 30 | /** 31 | * Input for the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) 32 | */ 33 | src: RequestInfo | string; 34 | /** 35 | * Optional key. 36 | * 37 | * Used to retrieve the {@link LoadedValue} using `AsyncPreloader.items.get(id)` 38 | */ 39 | id?: unknown; 40 | /** 41 | * Optional {@link LoaderKey}. 42 | * 43 | * If none specified, the loader is inferred from the file extension. 44 | * Default to `Text` if the extension doesn't match any of the extensions specified in {@link AsyncPreloader.loaders}. 45 | * 46 | * Note: It needs to be specified for Font and Audio (webm, ogg). 47 | */ 48 | loader?: LoaderKey; 49 | /** 50 | * Optional `RequestInit` object to pass to the [fetch method](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). 51 | */ 52 | options?: RequestInit; 53 | /** 54 | * Font options used by FontFace and FontFaceObserver 55 | */ 56 | fontOptions?: FontOptions; 57 | /** 58 | * Optional{@link BodyMethod} used to handle the Response. 59 | * 60 | * Default to `blob` for Image, Video and Audio. See {@link AsyncPreloader.defaultBodyMethod}. 61 | */ 62 | body?: BodyMethod; 63 | /** 64 | * Optional mimeType used to handle the Response. 65 | * 66 | * Note: Only used to parse the document in the Xml Loader. 67 | */ 68 | mimeType?: DOMParserSupportedType; 69 | /** 70 | * Optional disable [image decoding](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decode). 71 | * 72 | * Note: Only used for loadImage. 73 | */ 74 | noDecode?: boolean; 75 | } 76 | 77 | /** 78 | * Keys used for the {@link AsyncPreloader.loaders} 79 | */ 80 | export enum LoaderKey { 81 | Json = "Json", 82 | ArrayBuffer = "ArrayBuffer", 83 | Blob = "Blob", 84 | FormData = "FormData", 85 | Text = "Text", 86 | 87 | Image = "Image", 88 | Video = "Video", 89 | Audio = "Audio", 90 | Xml = "Xml", 91 | Font = "Font", 92 | } 93 | 94 | /** 95 | * Font options used by FontFace and FontFaceObserver 96 | */ 97 | export interface FontOptions { 98 | /** 99 | * [FontFace constructor descriptors](https://developer.mozilla.org/en-US/docs/Web/API/FontFace/FontFace) 100 | */ 101 | descriptors?: FontFaceDescriptors; 102 | /** 103 | * FontFaceObserver.FontVariant 104 | */ 105 | variant?: FontFaceObserver.FontVariant; 106 | /** 107 | * Argument for [FontFace.load](https://developer.mozilla.org/en-US/docs/Web/API/FontFace/load) 108 | */ 109 | testString?: string; 110 | /** 111 | * Argument for [FontFace.load](https://developer.mozilla.org/en-US/docs/Web/API/FontFace/load) 112 | */ 113 | timeout?: number; 114 | } 115 | 116 | /** 117 | * Values used for the {@link AsyncPreloader.loaders} 118 | */ 119 | export interface LoaderValue { 120 | /** 121 | * {@link LoadItem} with no loader key specified will use the following array to find which loader should be used. 122 | */ 123 | extensions: string[]; 124 | /** 125 | * Optional mimeType used to handle the Response. 126 | * 127 | * Note: Only used to parse the document in the Xml Loader. 128 | */ 129 | mimeType?: { [key: string]: DOMParserSupportedType }; 130 | /** 131 | * Optional defaultMimeType used to handle the Response. 132 | * 133 | * Note: Only used to parse the document in the Xml Loader. 134 | */ 135 | defaultMimeType?: DOMParserSupportedType; 136 | } 137 | -------------------------------------------------------------------------------- /test/assets/audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/async-preloader/c1a85109afec2a2500fc29044601caef31825333/test/assets/audio.mp3 -------------------------------------------------------------------------------- /test/assets/default.unknown: -------------------------------------------------------------------------------- 1 | test string 2 | -------------------------------------------------------------------------------- /test/assets/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/async-preloader/c1a85109afec2a2500fc29044601caef31825333/test/assets/font.ttf -------------------------------------------------------------------------------- /test/assets/image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/async-preloader/c1a85109afec2a2500fc29044601caef31825333/test/assets/image.gif -------------------------------------------------------------------------------- /test/assets/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/async-preloader/c1a85109afec2a2500fc29044601caef31825333/test/assets/image.jpg -------------------------------------------------------------------------------- /test/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/async-preloader/c1a85109afec2a2500fc29044601caef31825333/test/assets/image.png -------------------------------------------------------------------------------- /test/assets/json.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "json" 3 | } 4 | -------------------------------------------------------------------------------- /test/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "id": "myDefaultFile", 5 | "src": "/test/assets/default.unknown" 6 | }, 7 | { 8 | "id": "myTextFile", 9 | "src": "/test/assets/text.txt" 10 | }, 11 | { 12 | "id": "myJsonFile", 13 | "src": "/test/assets/json.json" 14 | }, 15 | { 16 | "id": "myXmlFile", 17 | "src": "/test/assets/xml.xml" 18 | }, 19 | { 20 | "id": "mySvgFile", 21 | "src": "/test/assets/xml.svg" 22 | }, 23 | { 24 | "id": "myHtmlFile", 25 | "src": "/test/assets/xml.html" 26 | }, 27 | { 28 | "id": "myXmlDefaulFile", 29 | "src": "/test/assets/xml.unknown", 30 | "loader": "Xml" 31 | } 32 | ], 33 | "custom": { 34 | "path": [ 35 | { 36 | "id": "myDefaultFile", 37 | "src": "/test/assets/default.unknown" 38 | }, 39 | { 40 | "id": "myTextFile", 41 | "src": "/test/assets/text.txt" 42 | }, 43 | { 44 | "id": "myJsonFile", 45 | "src": "/test/assets/json.json" 46 | }, 47 | { 48 | "id": "myXmlFile", 49 | "src": "/test/assets/xml.xml" 50 | }, 51 | { 52 | "id": "mySvgFile", 53 | "src": "/test/assets/xml.svg" 54 | }, 55 | { 56 | "id": "myHtmlFile", 57 | "src": "/test/assets/xml.html" 58 | }, 59 | { 60 | "id": "myXmlDefaulFile", 61 | "src": "/test/assets/xml.unknown", 62 | "loader": "Xml" 63 | } 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/assets/text.txt: -------------------------------------------------------------------------------- 1 | test string 2 | -------------------------------------------------------------------------------- /test/assets/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/async-preloader/c1a85109afec2a2500fc29044601caef31825333/test/assets/video.mp4 -------------------------------------------------------------------------------- /test/assets/xml.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/assets/xml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/assets/xml.unknown: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Arctic 5 | 13,000 6 | 1,200 7 | 8 | 9 | Atlantic 10 | 87,000 11 | 3,900 12 | 13 | 14 | Pacific 15 | 180,000 16 | 4,000 17 | 18 | 19 | Indian 20 | 75,000 21 | 3,900 22 | 23 | 24 | Southern 25 | 20,000 26 | 4,500 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/assets/xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Arctic 5 | 13,000 6 | 1,200 7 | 8 | 9 | Atlantic 10 | 87,000 11 | 3,900 12 | 13 | 14 | Pacific 15 | 180,000 16 | 4,000 17 | 18 | 19 | Indian 20 | 75,000 21 | 3,900 22 | 23 | 24 | Southern 25 | 20,000 26 | 4,500 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/browser.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment puppeteer 3 | */ 4 | import { jest } from "@jest/globals"; 5 | import "expect-puppeteer"; 6 | import pti from "puppeteer-to-istanbul"; 7 | import { getPortPromise } from "portfinder"; 8 | 9 | import { start } from "./server.js"; 10 | import { items, manifestSrc } from "./data.js"; 11 | 12 | jest.setTimeout(20000); 13 | 14 | describe("Browser", () => { 15 | let server; 16 | let url; 17 | 18 | beforeAll(async () => { 19 | const port = await getPortPromise(); 20 | server = await start({ port }); 21 | 22 | url = `http://localhost:${port}`; 23 | 24 | await page.coverage.startJSCoverage(); 25 | 26 | await page.goto(url); 27 | 28 | // page 29 | // .on("console", async (message) => { 30 | // if (message.text() !== "JSHandle@error") { 31 | // console.log( 32 | // `${message.type().substring(0, 3).toUpperCase()} ${message.text()}`, 33 | // ); 34 | // return; 35 | // } 36 | // const messages = await Promise.all( 37 | // message.args().map((arg) => arg.getProperty("message")), 38 | // ); 39 | // console.log( 40 | // `${message.type().substr(0, 3).toUpperCase()} error ${messages.filter(Boolean)}`, 41 | // ); 42 | // }) 43 | // .on("pageerror", ({ message }) => console.log(message)) 44 | // .on("response", (response) => 45 | // console.log(`${response.status()} ${response.url()}`), 46 | // ) 47 | // .on("requestfailed", (request) => 48 | // console.log(`${request.failure().errorText} ${request.url()}`), 49 | // ); 50 | }); 51 | 52 | afterAll(async () => { 53 | await new Promise((resolve, reject) => { 54 | server.close((error) => { 55 | if (error) return reject(error); 56 | resolve(); 57 | }); 58 | }); 59 | 60 | const jsCoverage = await page.coverage.stopJSCoverage(); 61 | pti.write( 62 | [...jsCoverage.filter((item) => item.url.startsWith(`${url}/lib`))], 63 | { 64 | includeHostname: false, 65 | storagePath: "./.nyc_output", 66 | }, 67 | ); 68 | }); 69 | 70 | it("should load a LoadItem with Blob loader and return instanceof Blob", async () => { 71 | const result = await page.evaluate(async (item) => { 72 | const { default: Preloader } = await importShim("./lib/index.js"); 73 | const data = await Preloader.loadBlob(item); 74 | return data.constructor.name; 75 | }, items.get("jpg")); 76 | 77 | expect(result).toEqual("Blob"); 78 | }); 79 | 80 | it("should load a LoadItem with Image loader and return HTMLImageElement", async () => { 81 | const result = await page.evaluate(async (item) => { 82 | const { default: Preloader } = await importShim("./lib/index.js"); 83 | const data = await Preloader.loadItem(item); 84 | return data.constructor.name; 85 | }, items.get("jpg")); 86 | 87 | expect(result).toEqual("HTMLImageElement"); 88 | }); 89 | 90 | it("should load a LoadItem with Image loader and body Blob and return HTMLImageElement", async () => { 91 | const result = await page.evaluate(async (item) => { 92 | const { default: Preloader } = await importShim("./lib/index.js"); 93 | const data = await Preloader.loadImage({ ...item, body: "blob" }); 94 | return data.constructor.name; 95 | }, items.get("jpg")); 96 | 97 | expect(result).toEqual("HTMLImageElement"); 98 | }); 99 | 100 | it("should try to load a LoadItem (jpg) with Image loader and throw decode error", async () => { 101 | const result = await page.evaluate(async (item) => { 102 | const { default: Preloader } = await importShim("./lib/index.js"); 103 | try { 104 | await Preloader.loadImage({ 105 | ...item, 106 | src: item.src.replace(".jpg", "unknown.jpg"), 107 | }); 108 | } catch (error) { 109 | return error.message; 110 | } 111 | return "Test didn't throw"; 112 | }, items.get("jpg")); 113 | 114 | expect(result).toEqual("The source image cannot be decoded."); 115 | }); 116 | 117 | it("should load a LoadItem with Image loader and body arrayBuffer and return ArrayBuffer", async () => { 118 | const result = await page.evaluate(async (item) => { 119 | const { default: Preloader } = await importShim("./lib/index.js"); 120 | const data = await Preloader.loadImage({ ...item, body: "arrayBuffer" }); 121 | return data.constructor.name; 122 | }, items.get("jpg")); 123 | 124 | expect(result).toEqual("ArrayBuffer"); 125 | }); 126 | 127 | it("should load a LoadItem with body = arrayBuffer with Image loader and return an ArrayBuffer", async () => { 128 | const result = await page.evaluate(async (item) => { 129 | const { default: Preloader } = await importShim("./lib/index.js"); 130 | const data = await Preloader.loadItem({ ...item, body: "arrayBuffer" }); 131 | return data.constructor.name; 132 | }, items.get("jpg")); 133 | 134 | expect(result).toEqual("ArrayBuffer"); 135 | }); 136 | 137 | it.skip("should load a LoadItem with Video loader and return HTMLVideoElement", async () => { 138 | const result = await page.evaluate(async (item) => { 139 | const { default: Preloader } = await importShim("./lib/index.js"); 140 | const data = await Preloader.loadItem(item); 141 | return data.constructor.name; 142 | }, items.get("mp4")); 143 | 144 | expect(result).toEqual("HTMLVideoElement"); 145 | }); 146 | 147 | it("should load a LoadItem with Audio loader and return HTMLAudioElement", async () => { 148 | const result = await page.evaluate(async (item) => { 149 | const { default: Preloader } = await importShim("./lib/index.js"); 150 | const data = await Preloader.loadItem(item); 151 | return data.constructor.name; 152 | }, items.get("mp3")); 153 | 154 | expect(result).toEqual("HTMLAudioElement"); 155 | }); 156 | 157 | it("should load a LoadItem with body = arrayBuffer with Audio Loader and return an ArrayBuffer", async () => { 158 | const result = await page.evaluate(async (item) => { 159 | const { default: Preloader } = await importShim("./lib/index.js"); 160 | const data = await Preloader.loadItem({ ...item, body: "arrayBuffer" }); 161 | return data.constructor.name; 162 | }, items.get("mp3")); 163 | 164 | expect(result).toEqual("ArrayBuffer"); 165 | }); 166 | 167 | it("should load a LoadItem (xml) with Xml Loader and return Document", async () => { 168 | const result = await page.evaluate(async (item) => { 169 | const { default: Preloader } = await importShim("./lib/index.js"); 170 | const data = await Preloader.loadItem(item); 171 | return { 172 | constructor: data.constructor.name, 173 | }; 174 | }, items.get("xml")); 175 | 176 | expect(result.constructor).toEqual("XMLDocument"); 177 | // expect(result.data).toEqual(expected.get("xml")); 178 | }); 179 | 180 | it("should load a LoadItem (html) with Xml Loader and return Document", async () => { 181 | const result = await page.evaluate(async (item) => { 182 | const { default: Preloader } = await importShim("./lib/index.js"); 183 | const data = await Preloader.loadItem(item); 184 | return { 185 | constructor: data.constructor.name, 186 | }; 187 | }, items.get("html")); 188 | 189 | expect(result.constructor).toEqual("HTMLDocument"); 190 | // expect(data).toEqual(expected.get("html")); 191 | }); 192 | 193 | it("should load a LoadItem (svg) with Xml Loader and return Document", async () => { 194 | const result = await page.evaluate(async (item) => { 195 | const { default: Preloader } = await importShim("./lib/index.js"); 196 | const data = await Preloader.loadItem(item); 197 | return { 198 | constructor: data.constructor.name, 199 | }; 200 | }, items.get("svg")); 201 | 202 | expect(result.constructor).toEqual("XMLDocument"); 203 | // expect(data).toEqual(expected.get("svg")); 204 | }); 205 | 206 | it("should load a LoadItem (svg file with a special extension) with Xml Loader and return Document", async () => { 207 | const result = await page.evaluate(async (item) => { 208 | const { default: Preloader } = await importShim("./lib/index.js"); 209 | const data = await Preloader.loadItem(item); 210 | return { 211 | constructor: data.constructor.name, 212 | }; 213 | }, items.get("defaultXml")); 214 | 215 | expect(result.constructor).toEqual("XMLDocument"); 216 | // expect(data).toEqual(expected.get("xml")); 217 | }); 218 | 219 | it("should load a LoadItem (ttf) with Font Loader and return FontFace", async () => { 220 | const result = await page.evaluate(async (item) => { 221 | const { default: Preloader } = await importShim("./lib/index.js"); 222 | const data = await Preloader.loadItem(item); 223 | return { 224 | constructor: data.constructor.name, 225 | family: data.family, 226 | }; 227 | }, items.get("font")); 228 | 229 | expect(result.constructor).toEqual("FontFace"); 230 | expect(result.family).toEqual("myFont"); 231 | }); 232 | 233 | it("should load a LoadItem (ttf) by src only with Font Loader and return FontFace", async () => { 234 | const result = await page.evaluate( 235 | async (item) => { 236 | const { default: Preloader } = await importShim("./lib/index.js"); 237 | const data = await Preloader.loadItem(item); 238 | return { 239 | constructor: data.constructor.name, 240 | family: data.family, 241 | }; 242 | }, 243 | { src: items.get("font").src }, 244 | ); 245 | 246 | expect(result.constructor).toEqual("FontFace"); 247 | expect(result.family).toEqual("font"); 248 | }); 249 | 250 | it("should load a LoadItem (ttf buffer) with Font Loader and return FontFace", async () => { 251 | const result = await page.evaluate(async (item) => { 252 | const { default: Preloader } = await importShim("./lib/index.js"); 253 | const data = await Preloader.loadItem({ ...item, body: "arrayBuffer" }); 254 | return { 255 | constructor: data.constructor.name, 256 | family: data.family, 257 | }; 258 | }, items.get("font")); 259 | 260 | expect(result.constructor).toEqual("FontFace"); 261 | expect(result.family).toEqual("myFont"); 262 | }); 263 | 264 | it("should load a LoadItem (font observer) with Font Loader and return FontFace", async () => { 265 | const item = items.get("fontface"); 266 | await page.addStyleTag({ 267 | content: /* css */ `@font-face { 268 | font-family: '${item.id}'; 269 | src: url(${items.get("font").src}) format('truetype'); 270 | }`, 271 | }); 272 | 273 | const result = await page.evaluate(async (item) => { 274 | const { default: Preloader } = await importShim("./lib/index.js"); 275 | const data = await Preloader.loadItem(item); 276 | return data; 277 | }, item); 278 | 279 | expect(result).toEqual(item.id); 280 | }); 281 | 282 | it("should load a manifest, load its items and return an array of LoadedValue", async () => { 283 | const result = await page.evaluate(async (item) => { 284 | const { default: Preloader } = await importShim("./lib/index.js"); 285 | const data = await Preloader.loadManifest(item); 286 | return data; 287 | }, `${url}/${manifestSrc}`); 288 | 289 | expect(result).toHaveLength(7); 290 | // expect(result).toEqual( 291 | // expect.arrayContaining(Array.from(expected.values())) 292 | // ); 293 | }); 294 | 295 | it("should load a manifest with a specified data path, load its items and return an array of LoadedValue", async () => { 296 | const result = await page.evaluate(async (item) => { 297 | const { default: Preloader } = await importShim("./lib/index.js"); 298 | const data = await Preloader.loadManifest(item, "custom.path"); 299 | return data; 300 | }, `${url}/${manifestSrc}`); 301 | 302 | expect(result).toHaveLength(7); 303 | // expect(result).toEqual( 304 | // expect.arrayContaining(Array.from(expected.values())) 305 | // ); 306 | }); 307 | 308 | it("should cancel loading an array of LoadItem and return an CancelLoading", async () => { 309 | let itemsToLoad = Array.from(items.values()); 310 | 311 | const result = await page.evaluate(async (items) => { 312 | const { default: Preloader } = await importShim("./lib/index.js"); 313 | 314 | const controller = new AbortController(); 315 | const { signal } = controller; 316 | 317 | const load = async () => { 318 | try { 319 | await Preloader.loadItems( 320 | items.map((item) => ({ 321 | ...item, 322 | options: { 323 | ...(item.options || {}), 324 | signal, 325 | }, 326 | })), 327 | ); 328 | } catch (error) { 329 | return error; 330 | } 331 | return "NoError"; 332 | }; 333 | 334 | const results = await Promise.allSettled([ 335 | (async () => { 336 | controller.abort("CancelLoading"); 337 | })(), 338 | load(), 339 | ]); 340 | 341 | return results[1].value; 342 | }, itemsToLoad); 343 | 344 | expect(result).toEqual("CancelLoading"); 345 | }); 346 | }); 347 | -------------------------------------------------------------------------------- /test/data.js: -------------------------------------------------------------------------------- 1 | import { AsyncPreloader } from "../src"; 2 | import { DOMParser } from "@xmldom/xmldom"; 3 | 4 | const pathname = "test/assets/"; 5 | 6 | export const items = new Map() 7 | .set("default", { id: "myDefaultFile", src: `${pathname}default.unknown` }) 8 | .set("txt", { id: "myTextFile", src: `${pathname}text.txt` }) 9 | .set("json", { id: "myJsonFile", src: `${pathname}json.json` }) 10 | .set("jpg", { id: "myImageFile", src: `${pathname}image.jpg` }) 11 | .set("mp4", { id: "myVideoFile", src: `${pathname}video.mp4` }) 12 | .set("mp3", { id: "myAudioFile", src: `${pathname}audio.mp3` }) 13 | .set("xml", { id: "myXmlFile", src: `${pathname}xml.xml` }) 14 | .set("svg", { id: "mySvgFile", src: `${pathname}xml.svg` }) 15 | .set("html", { id: "myHtmlFile", src: `${pathname}xml.html` }) 16 | .set("defaultXml", { 17 | id: "myXmlDefaultFile", 18 | src: `${pathname}xml.unknown`, 19 | loader: "Xml", 20 | }) 21 | .set("font", { id: "myFont", src: `${pathname}font.ttf` }) 22 | .set("fontface", { 23 | id: "Space Regular", 24 | loader: "Font", 25 | fontOptions: { timeout: 10000 }, 26 | }); 27 | 28 | export const expected = new Map() 29 | .set("string", `test string\n`) 30 | .set( 31 | "html", 32 | new DOMParser().parseFromString( 33 | /* html */ ` 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | \n`, 45 | "text/html" 46 | ) 47 | ) 48 | .set( 49 | "svg", 50 | new DOMParser().parseFromString( 51 | /* html */ ` 52 | 53 | 54 | 55 | 56 | \n`, 57 | "image/svg+xml" 58 | ) 59 | ) 60 | .set( 61 | "xml", 62 | new DOMParser().parseFromString( 63 | /* xml */ ` 64 | 65 | 66 | Arctic 67 | 13,000 68 | 1,200 69 | 70 | 71 | Atlantic 72 | 87,000 73 | 3,900 74 | 75 | 76 | Pacific 77 | 180,000 78 | 4,000 79 | 80 | 81 | Indian 82 | 75,000 83 | 3,900 84 | 85 | 86 | Southern 87 | 20,000 88 | 4,500 89 | 90 | 91 | 92 | \n`, 93 | "application/xml" 94 | ) 95 | ) 96 | .set( 97 | "defaultXml", 98 | new DOMParser().parseFromString( 99 | /* xml */ ` 100 | 101 | 102 | Arctic 103 | 13,000 104 | 1,200 105 | 106 | 107 | Atlantic 108 | 87,000 109 | 3,900 110 | 111 | 112 | Pacific 113 | 180,000 114 | 4,000 115 | 116 | 117 | Indian 118 | 75,000 119 | 3,900 120 | 121 | 122 | Southern 123 | 20,000 124 | 4,500 125 | 126 | 127 | 128 | \n`, 129 | "application/xml" 130 | ) 131 | ) 132 | .set("json", { test: "json" }); 133 | 134 | export const manifestSrc = `${pathname}manifest.json`; 135 | 136 | // Node 137 | const filterNodeSupportedItems = (key) => 138 | ["default", "txt", "json", "string"].includes(key); 139 | 140 | export const getNodeSupportedItems = (items) => 141 | Array.from( 142 | new Map([...items].filter(([k]) => filterNodeSupportedItems(k))).values() 143 | ); 144 | 145 | export const expectedNode = new Map( 146 | [...expected].filter(([k]) => filterNodeSupportedItems(k)) 147 | ); 148 | -------------------------------------------------------------------------------- /test/global-setup.js: -------------------------------------------------------------------------------- 1 | import { promisify } from "util"; 2 | import { exec as execCb } from "child_process"; 3 | import setupPuppeteer from "jest-environment-puppeteer/setup"; 4 | 5 | const exec = promisify(execCb); 6 | 7 | export default async function globalSetup(globalConfig) { 8 | const { stdout, stderr } = await exec("npm run coverage"); 9 | if (stderr) throw new Error(stderr); 10 | console.log(stdout); 11 | await setupPuppeteer(globalConfig); 12 | } 13 | -------------------------------------------------------------------------------- /test/global-teardown.js: -------------------------------------------------------------------------------- 1 | import { promisify } from "util"; 2 | import { exec as execCb } from "child_process"; 3 | import teardownPuppeteer from "jest-environment-puppeteer/teardown"; 4 | 5 | const exec = promisify(execCb); 6 | 7 | export default async function globalTeardown(globalConfig) { 8 | const { stdout, stderr } = await exec("npm run coverage"); 9 | if (stderr) throw new Error(stderr); 10 | console.log(stdout); 11 | await teardownPuppeteer(globalConfig); 12 | } 13 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import { getPortPromise } from "portfinder"; 2 | 3 | import Preloader, { AsyncPreloader } from "../src"; 4 | import { 5 | items as dataItems, 6 | expectedNode, 7 | getNodeSupportedItems, 8 | } from "./data.js"; 9 | import { start } from "./server.js"; 10 | 11 | // Suite 12 | describe("AsyncPreloader", () => { 13 | let server; 14 | let url; 15 | let items; 16 | 17 | beforeAll(async () => { 18 | const port = await getPortPromise(); 19 | server = await start({ port }); 20 | 21 | url = `http://localhost:${port}`; 22 | 23 | items = new Map( 24 | Array.from(dataItems, ([key, value]) => [ 25 | key, 26 | { ...value, src: `${url}/${value.src}` }, 27 | ]) 28 | ); 29 | }); 30 | 31 | afterAll(async () => { 32 | await new Promise((resolve, reject) => { 33 | server.close((error) => { 34 | if (error) return reject(error); 35 | resolve(); 36 | }); 37 | }); 38 | }); 39 | 40 | describe("Loader", () => { 41 | afterEach(() => { 42 | Preloader.items.clear(); 43 | }); 44 | 45 | describe("for several items", () => { 46 | it("should load an array of LoadItem and return an array of LoadedValue", async () => { 47 | expect.assertions(3); 48 | 49 | const itemsToLoad = getNodeSupportedItems(items); 50 | const data = await Preloader.loadItems(itemsToLoad); 51 | 52 | expect(data).toBeInstanceOf(Array); 53 | expect(data).toHaveLength(itemsToLoad.length); 54 | expect(data).toEqual( 55 | expect.arrayContaining(Array.from(expectedNode.values())) 56 | ); 57 | }); 58 | it("should load an array of strings and return an array of LoadedValue", async () => { 59 | expect.assertions(3); 60 | 61 | const itemsToLoad = getNodeSupportedItems(items); 62 | const data = await Preloader.loadItems( 63 | itemsToLoad.map(({ src }) => src) 64 | ); 65 | 66 | expect(data).toBeInstanceOf(Array); 67 | expect(data).toHaveLength(itemsToLoad.length); 68 | expect(data).toEqual( 69 | expect.arrayContaining(Array.from(expectedNode.values())) 70 | ); 71 | }); 72 | }); 73 | 74 | describe("for default fetch methods", () => { 75 | it("should load a LoadItem with Json loader and return the JSON Object", async () => { 76 | expect.assertions(1); 77 | 78 | const item = items.get("json"); 79 | 80 | const data = await Preloader.loadJson(item); 81 | expect(data).toEqual(expectedNode.get("json")); 82 | }); 83 | 84 | it("should load a LoadItem with Text loader and return the string", async () => { 85 | expect.assertions(1); 86 | 87 | const item = items.get("txt"); 88 | 89 | const data = await Preloader.loadText(item); 90 | expect(data).toBe(expectedNode.get("string")); 91 | }); 92 | 93 | it("should load a LoadItem with ArrayBuffer loader and return instanceof ArrayBuffer", async () => { 94 | expect.assertions(1); 95 | 96 | const item = items.get("mp3"); 97 | 98 | const data = await Preloader.loadArrayBuffer(item); 99 | expect(data.constructor.name).toEqual(ArrayBuffer.name); 100 | }); 101 | 102 | it("should load a LoadItem with Blob loader and return instanceof Blob", async () => { 103 | expect.assertions(1); 104 | 105 | const item = items.get("jpg"); 106 | 107 | const data = await Preloader.loadBlob(item); 108 | 109 | expect(data.constructor.name).toEqual(Blob.name); 110 | }); 111 | 112 | it.skip("should load a LoadItem with FormData loader and return instanceof FormData", async () => { 113 | expect.assertions(1); 114 | 115 | const formData = new FormData(); 116 | formData.set("greeting", "Hello, world!"); 117 | 118 | const item = { 119 | src: "https://httpbin.org/post", 120 | options: { 121 | method: "POST", 122 | body: new URLSearchParams(formData), 123 | }, 124 | }; 125 | 126 | const data = await Preloader.loadFormData(item); 127 | 128 | expect(data.constructor.name).toEqual(FormData.name); 129 | }); 130 | }); 131 | 132 | describe("for a single item", () => { 133 | it("should load a LoadItem with Default loader and return a string", async () => { 134 | expect.assertions(1); 135 | 136 | const item = items.get("default"); 137 | 138 | const data = await Preloader.loadItem(item); 139 | expect(data).toBe(expectedNode.get("string")); 140 | }); 141 | 142 | it("should load a LoadItem with Json loader and return the JSON Object", async () => { 143 | expect.assertions(1); 144 | 145 | const item = items.get("json"); 146 | 147 | const data = await Preloader.loadItem(item); 148 | expect(data).toEqual(expectedNode.get("json")); 149 | }); 150 | 151 | it("should load a LoadItem with body = arrayBuffer with Audio Loader and return an ArrayBuffer", async () => { 152 | expect.assertions(1); 153 | 154 | const item = items.get("mp3"); 155 | item.body = "arrayBuffer"; 156 | 157 | const data = await Preloader.loadItem(item); 158 | expect(data.constructor.name).toEqual(ArrayBuffer.name); 159 | }); 160 | it("should load a LoadItem with Text Loader and return String", async () => { 161 | expect.assertions(1); 162 | 163 | const item = items.get("txt"); 164 | 165 | const data = await Preloader.loadItem(item); 166 | expect(data).toBe(expectedNode.get("string")); 167 | }); 168 | 169 | it("should try to load a LoadItem (ttf) with FontFace Loader and throw", async () => { 170 | expect.assertions(1); 171 | 172 | await expect(Preloader.loadItem(items.get("font"))).rejects.toThrow( 173 | "FontFace is not defined" 174 | ); 175 | }); 176 | }); 177 | 178 | describe("for edge cases", () => { 179 | it("should load a LoadItem without id with Default loader and return a string", async () => { 180 | expect.assertions(1); 181 | 182 | const item = items.get("default"); 183 | 184 | const data = await Preloader.loadItem({ src: item.src }); 185 | expect(data).toBe(expectedNode.get("string")); 186 | }); 187 | 188 | it("should load an array of LoadItem and update a loadedCount variable", async () => { 189 | expect.assertions(1); 190 | 191 | const itemsToLoad = getNodeSupportedItems(items); 192 | 193 | let loadedCount = 0; 194 | async function preload() { 195 | await Promise.all( 196 | itemsToLoad.map(async (item) => { 197 | const data = await Preloader.loadItem(item); 198 | loadedCount++; 199 | // console.log(`Progress: ${100 * loadedCount / itemsToLoad.length}%`); 200 | }) 201 | ); 202 | } 203 | await preload(); 204 | expect(loadedCount).toEqual(itemsToLoad.length); 205 | }); 206 | }); 207 | }); 208 | 209 | describe("Static methods", () => { 210 | describe("Utils", () => { 211 | it("should return the file extension from file path", () => { 212 | const data = AsyncPreloader.getFileExtension("assets/json.json"); 213 | expect(data).toBe("json"); 214 | }); 215 | 216 | it("should return null from file path with no extension", () => { 217 | const data = AsyncPreloader.getFileExtension("assets/default"); 218 | expect(data).toBe(null); 219 | }); 220 | }); 221 | 222 | describe("Get loader key", () => { 223 | it("should return the loader from file extension", () => { 224 | const data = AsyncPreloader.getLoaderKey("json"); 225 | expect(data).toBe("Json"); 226 | }); 227 | 228 | it("should return the default loader from null", () => { 229 | const data = AsyncPreloader.getLoaderKey(null); 230 | expect(data).toBe("Text"); 231 | }); 232 | }); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises"; 2 | import http from "node:http"; 3 | import { join, dirname } from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | 6 | import mime from "mime-types"; 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)); 9 | 10 | const start = async ({ hostname = "localhost", port = 3000 } = {}) => { 11 | const server = http.createServer(async (req, res) => { 12 | if (req.url === "/") req.url = "/index.html"; 13 | 14 | try { 15 | const data = await readFile(join(__dirname, "..", req.url)); 16 | res.writeHead(200, { "Content-Type": mime.lookup(req.url) }); 17 | res.end(data); 18 | return; 19 | } catch (error) { 20 | if (error) { 21 | res.writeHead(404); 22 | res.end(JSON.stringify(error)); 23 | return; 24 | } 25 | } 26 | }); 27 | 28 | return new Promise((resolve, reject) => { 29 | server.listen(port, hostname, (error) => { 30 | if (error) return reject(error); 31 | console.log(`Server running at http://${hostname}:${port}/`); 32 | resolve(server); 33 | }); 34 | }); 35 | }; 36 | 37 | export { start }; 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "declarationDir": "types", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "importHelpers": true, 10 | "lib": ["DOM", "ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021"], 11 | "module": "ES2020", 12 | "moduleResolution": "node", 13 | "outDir": "lib", 14 | "sourceMap": true, 15 | "strictFunctionTypes": true, 16 | "target": "ES2021" 17 | }, 18 | "include": [ 19 | "src/**/*" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "web_modules", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------