├── .browserslistrc ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── api-extractor.json ├── babel.config.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── index.ts ├── test ├── index.spec.js └── scripts │ ├── bar.js │ ├── baz.js │ └── foo.js └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 9 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.local* 2 | .temp 3 | coverage 4 | dist 5 | node_modules 6 | test/scripts 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | 'airbnb-typescript/base', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | project: 'tsconfig.json', 15 | sourceType: 'module', 16 | }, 17 | plugins: [ 18 | '@typescript-eslint', 19 | ], 20 | rules: { 21 | '@typescript-eslint/no-var-requires': 'off', 22 | }, 23 | overrides: [ 24 | { 25 | files: ['test/**/*.spec.js'], 26 | env: { 27 | mocha: true, 28 | }, 29 | globals: { 30 | expect: true, 31 | }, 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 14 18 | - run: npm install 19 | - run: npm run lint 20 | - run: npm run build 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.local* 2 | *.log 3 | *.map 4 | .DS_Store 5 | .temp 6 | coverage 7 | dist 8 | node_modules 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.0.0](https://github.com/fengyuanchen/load-scripts/compare/v1.1.0...v2.0.0) (2021-09-21) 2 | 3 | 4 | * refactor!: migrate to TypeScript ([0550177](https://github.com/fengyuanchen/load-scripts/commit/05501779c109b2a618bd971a5fbab43ef6903a80)) 5 | 6 | 7 | ### BREAKING CHANGES 8 | 9 | * drop `dist/load-scripts.common.js` from the package 10 | 11 | 12 | 13 | # [1.1.0](https://github.com/fengyuanchen/load-scripts/compare/v1.0.1...v1.1.0) (2021-09-20) 14 | 15 | 16 | ### Features 17 | 18 | * add type definitions for TypeScript ([700e649](https://github.com/fengyuanchen/load-scripts/commit/700e649c1e9599d3282a6d22a6dbe61f99d5093c)) 19 | * improve script url matching ([fd78f8c](https://github.com/fengyuanchen/load-scripts/commit/fd78f8c048c251d62a2938d12719822b7e192b3b)) 20 | 21 | 22 | 23 | ## [1.0.1](https://github.com/fengyuanchen/load-scripts/compare/v1.0.0...v1.0.1) (2021-09-19) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * avoid loading script repeatedly ([626ae03](https://github.com/fengyuanchen/load-scripts/commit/626ae03fd5ad73b84f3be0b0a57b8e11ec4eed2e)) 29 | 30 | 31 | 32 | # [1.0.0](https://github.com/fengyuanchen/load-scripts/compare/v0.1.0...v1.0.0) (2018-05-21) 33 | 34 | 35 | 36 | # 0.1.0 (2018-04-11) 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2018-present Chen Fengyuan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # load-scripts 2 | 3 | [![Downloads](https://img.shields.io/npm/dm/load-scripts.svg)](https://www.npmjs.com/package/load-scripts) [![Version](https://img.shields.io/npm/v/load-scripts.svg)](https://www.npmjs.com/package/load-scripts) [![Gzip Size](https://img.shields.io/bundlephobia/minzip/load-scripts.svg)](https://unpkg.com/load-scripts/dist/load-scripts.js) 4 | 5 | > Dynamic scripts loading for modern browsers. 6 | 7 | ## Main files 8 | 9 | ```text 10 | dist/ 11 | ├── load-scripts.js (UMD, default) 12 | ├── load-scripts.min.js (UMD, compressed) 13 | ├── load-scripts.esm.js (ECMAScript Module) 14 | ├── load-scripts.esm.min.js (ECMAScript Module, compressed) 15 | └── load-scripts.d.ts (TypeScript Declaration File) 16 | ``` 17 | 18 | ## Getting started 19 | 20 | ### Installation 21 | 22 | ```shell 23 | npm install load-scripts 24 | ``` 25 | 26 | In browser: 27 | 28 | ```html 29 | 30 | ``` 31 | 32 | ### Usage 33 | 34 | #### Syntax 35 | 36 | ```js 37 | loadScripts(script1, script2, ..., scriptN) 38 | .then(() => {}) 39 | .catch((err) => {}) 40 | .finally(() => {}); 41 | ``` 42 | 43 | #### Example 44 | 45 | ```js 46 | import loadScripts from 'load-scripts'; 47 | 48 | loadScripts('foo.js').then(() => { 49 | console.log(window.Foo); 50 | }); 51 | 52 | loadScripts('foo.js', 'bar.js').then(() => { 53 | console.log(window.Foo, window.Bar); 54 | }); 55 | ``` 56 | 57 | In browser: 58 | 59 | ```html 60 | 65 | ``` 66 | 67 | ## Browser support 68 | 69 | - Chrome (latest) 70 | - Firefox (latest) 71 | - Safari (latest) 72 | - Opera (latest) 73 | - Edge (latest) 74 | - Internet Explorer 10+ (requires a `Promise` polyfill as [es6-promise](https://github.com/stefanpenner/es6-promise)) 75 | 76 | ## Versioning 77 | 78 | Maintained under the [Semantic Versioning guidelines](https://semver.org/). 79 | 80 | ## License 81 | 82 | [MIT](https://opensource.org/licenses/MIT) © [Chen Fengyuan](https://chenfengyuan.com/) 83 | -------------------------------------------------------------------------------- /api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "mainEntryPointFilePath": "./.temp/index.d.ts", 3 | "apiReport": { 4 | "enabled": false 5 | }, 6 | "docModel": { 7 | "enabled": false 8 | }, 9 | "dtsRollup": { 10 | "enabled": true, 11 | "publicTrimmedFilePath": "./dist/.d.ts" 12 | }, 13 | "tsdocMetadata": { 14 | "enabled": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env'], 3 | }; 4 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | process.env.CHROME_BIN = puppeteer.executablePath(); 4 | process.env.NODE_ENV = 'test'; 5 | 6 | module.exports = (config) => { 7 | config.set({ 8 | autoWatch: false, 9 | browsers: ['ChromeHeadless'], 10 | files: [ 11 | 'dist/load-scripts.js', 12 | 'test/index.spec.js', 13 | { 14 | pattern: 'test/scripts/*', 15 | included: false, 16 | }, 17 | ], 18 | frameworks: ['mocha', 'chai'], 19 | reporters: ['mocha'], 20 | singleRun: true, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "load-scripts", 3 | "version": "2.0.0", 4 | "description": "Dynamic scripts loading for modern browsers.", 5 | "main": "dist/load-scripts.js", 6 | "module": "dist/load-scripts.esm.js", 7 | "types": "dist/load-scripts.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build": "rollup -c --environment BUILD:production", 13 | "build:api": "api-extractor run --local --verbose", 14 | "build:dts": "tsc --outDir ./.temp --declaration --emitDeclarationOnly", 15 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 16 | "clean": "del-cli dist .temp", 17 | "lint": "eslint . --ext .js,.ts --fix", 18 | "release": "npm run clean && npm run lint && npm run build:dts && npm run build && npm run build:api && npm test && npm run changelog", 19 | "test": "karma start karma.conf.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/fengyuanchen/load-scripts.git" 24 | }, 25 | "keywords": [ 26 | "load", 27 | "script", 28 | "scripts", 29 | "dynamic", 30 | "async", 31 | "promise", 32 | "browser" 33 | ], 34 | "author": { 35 | "name": "Chen Fengyuan", 36 | "url": "https://chenfengyuan.com/" 37 | }, 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/fengyuanchen/load-scripts/issues" 41 | }, 42 | "homepage": "https://github.com/fengyuanchen/load-scripts", 43 | "devDependencies": { 44 | "@babel/core": "^7.15.5", 45 | "@babel/preset-env": "^7.15.6", 46 | "@microsoft/api-extractor": "^7.18.9", 47 | "@rollup/plugin-typescript": "^8.2.5", 48 | "@typescript-eslint/eslint-plugin": "^4.31.2", 49 | "@typescript-eslint/parser": "^4.31.2", 50 | "chai": "^4.3.4", 51 | "change-case": "^4.1.2", 52 | "conventional-changelog-cli": "^2.1.1", 53 | "create-banner": "^2.0.0", 54 | "del-cli": "^4.0.1", 55 | "eslint": "^7.32.0", 56 | "eslint-config-airbnb-base": "^14.2.1", 57 | "eslint-config-airbnb-typescript": "^14.0.0", 58 | "eslint-plugin-import": "^2.24.2", 59 | "karma": "^6.3.4", 60 | "karma-chai": "^0.1.0", 61 | "karma-chrome-launcher": "^3.1.0", 62 | "karma-mocha": "^2.0.1", 63 | "karma-mocha-reporter": "^2.2.5", 64 | "mocha": "^9.1.1", 65 | "puppeteer": "^10.2.0", 66 | "rollup": "^2.56.3", 67 | "rollup-plugin-terser": "^7.0.2", 68 | "tslib": "^2.3.1", 69 | "typescript": "^4.4.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import createBanner from 'create-banner'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import { camelCase } from 'change-case'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import pkg from './package.json'; 6 | 7 | const name = camelCase(pkg.name); 8 | const banner = createBanner({ 9 | data: { 10 | year: '2018-present', 11 | }, 12 | template: 'inline', 13 | }); 14 | 15 | export default ['umd', 'esm'].map((format) => ({ 16 | input: 'src/index.ts', 17 | output: ['development', 'production'].map((mode) => { 18 | const output = { 19 | banner, 20 | format, 21 | name, 22 | file: pkg.main, 23 | }; 24 | 25 | if (format === 'esm') { 26 | output.file = pkg.module; 27 | } 28 | 29 | if (mode === 'production') { 30 | output.compact = true; 31 | output.file = output.file.replace(/(\.js)$/, '.min$1'); 32 | output.plugins = [ 33 | terser(), 34 | ]; 35 | } 36 | 37 | return output; 38 | }), 39 | plugins: [ 40 | typescript(), 41 | ], 42 | })); 43 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export default function loadScripts(...urls: string[]): Promise { 2 | return Promise.all(urls.map((url) => new Promise((resolve, reject) => { 3 | const parent = document.head || document.body || document.documentElement; 4 | 5 | // Avoid loading script repeatedly 6 | if (parent.querySelector(`script[src*="${url}"]`)) { 7 | resolve(url); 8 | return; 9 | } 10 | 11 | const script = document.createElement('script'); 12 | const loadend = () => { 13 | script.onerror = null; 14 | script.onload = null; 15 | }; 16 | 17 | script.onerror = () => { 18 | loadend(); 19 | reject(new Error(`Failed to load script: ${url}`)); 20 | }; 21 | script.onload = () => { 22 | loadend(); 23 | resolve(url); 24 | }; 25 | script.async = true; 26 | script.src = url; 27 | parent.appendChild(script); 28 | }))); 29 | } 30 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('load-scripts', () => { 2 | it('should load single script correctly', (done) => { 3 | window.loadScripts('/base/test/scripts/foo.js').then(() => { 4 | expect(window.Foo).to.be.a('function'); 5 | done(); 6 | }); 7 | }); 8 | 9 | it('should load multiple scripts correctly', (done) => { 10 | window.loadScripts('/base/test/scripts/bar.js', '/base/test/scripts/baz.js').then(() => { 11 | expect(window.Bar).to.be.a('function'); 12 | expect(window.Baz).to.be.a('function'); 13 | done(); 14 | }); 15 | }); 16 | 17 | it('should throw error', (done) => { 18 | const url = '/base/test/scripts/qux.js'; 19 | 20 | window.loadScripts(url).catch((error) => { 21 | expect(error.message).to.include(url); 22 | done(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/scripts/bar.js: -------------------------------------------------------------------------------- 1 | window.Bar = () => {}; 2 | -------------------------------------------------------------------------------- /test/scripts/baz.js: -------------------------------------------------------------------------------- 1 | window.Baz = () => {}; 2 | -------------------------------------------------------------------------------- /test/scripts/foo.js: -------------------------------------------------------------------------------- 1 | window.Foo = () => {}; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "moduleResolution": "node", 5 | "resolveJsonModule": true, 6 | "strict": true, 7 | "target": "esnext", 8 | }, 9 | "include": ["*.js", ".*.js", "src", "test"] 10 | } 11 | --------------------------------------------------------------------------------