├── .eslintignore ├── .prettierignore ├── test ├── constants.ts ├── removeScript.spec.ts ├── attrs.spec.ts ├── array.spec.ts ├── placement.spec.ts └── basic.spec.ts ├── browser ├── index.ts └── index.html ├── vite.config.ts ├── .prettierrc.json ├── vitest.config.ts ├── .gitignore ├── tsconfig.json ├── rollup.config.ts ├── .eslintrc.json ├── changelog.md ├── LICENSE ├── .github └── workflows │ └── check.yml ├── package.json ├── src └── index.ts └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /test/constants.ts: -------------------------------------------------------------------------------- 1 | export const TIMEOUT = 60_000; 2 | -------------------------------------------------------------------------------- /browser/index.ts: -------------------------------------------------------------------------------- 1 | import simpleLoadScript from '../src/index.js'; 2 | 3 | declare global { 4 | interface Window { 5 | simpleLoadScript: typeof simpleLoadScript; 6 | } 7 | } 8 | 9 | window.simpleLoadScript = simpleLoadScript; 10 | 11 | export {}; 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfigExport, defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | root: 'browser', 5 | server: { 6 | port: 3030, 7 | }, 8 | preview: { 9 | port: 8080, 10 | }, 11 | }) satisfies UserConfigExport; 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 80, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "bracketSameLine": false, 9 | "endOfLine": "lf", 10 | "tabWidth": 4 11 | } 12 | -------------------------------------------------------------------------------- /browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test Page 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { UserConfigExport, defineConfig } from 'vite'; 4 | import { TIMEOUT } from './test/constants'; 5 | 6 | export default defineConfig({ 7 | test: { 8 | testTimeout: TIMEOUT, 9 | hookTimeout: TIMEOUT, 10 | }, 11 | }) satisfies UserConfigExport; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # typical suspects 2 | **/node_modules/ 3 | **/tmp/ 4 | **/dist/ 5 | **/build/ 6 | **/out/ 7 | **/.next/ 8 | **/.vercel/ 9 | .vscode 10 | 11 | # testing 12 | /coverage 13 | /test-results 14 | 15 | # misc 16 | .DS_Store 17 | *.pem 18 | 19 | # logs 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | pnpm-debug.log* 24 | 25 | # tf 26 | .env* 27 | .vercel 28 | *.tsbuildinfo 29 | stats.html 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "es2015", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "noEmit": true, 11 | "moduleResolution": "node" 12 | }, 13 | "include": ["./**/*.ts"], 14 | "exclude": ["./dist", "./browser/dist"] 15 | } 16 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | export default { 5 | input: './src/index.ts', 6 | plugins: [terser(), typescript({ include: ['./src/index.ts'] })], 7 | output: [ 8 | { dir: './dist', format: 'cjs', exports: 'default' }, 9 | { dir: './dist', format: 'es' }, 10 | { 11 | dir: './dist', 12 | format: 'umd', 13 | name: 'simpleLoadScript', 14 | }, 15 | ], 16 | watch: { 17 | include: './src/**', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/stylistic", 11 | "prettier" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2021, 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["@typescript-eslint", "prettier"], 19 | "rules": { 20 | "prettier/prettier": "error" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | 3 | ## 2.1.1 4 | 5 | - update changelog 6 | 7 | ## 2.1.0 8 | 9 | - add action status badge 10 | - use oxlint instead of ESLint (leave config file) 11 | - update deps 12 | 13 | ## 2.0.0 14 | 15 | - **BREAKING CHANGE** exported files paths 16 | - **BREAKING CHANGE** `jsonp` removed form Config 17 | - **BREAKING CHANGE** `callBackName` removed form Config 18 | - **BREAKING CHANGE** `removeScript` changes - script is not removed by default on an error 19 | - **BREAKING CHANGE** `callBack` removed form Config - just run code after simpleLoadScript run (eg. `console.log` in examples) 20 | - TS 21 | - rollup 22 | - deps 23 | - tests 24 | - readme 25 | 26 | ## 1.0.3 27 | 28 | - update 2do 29 | - update readme 30 | - update dev deps 31 | 32 | ## 1.0.2 33 | 34 | - added `insertInto` config option 35 | 36 | ## 1.0.1 37 | 38 | - rename index.js 39 | - readme 40 | - changes in `all` method 41 | 42 | ## 1.0.0 43 | 44 | - `dontRemoveCallBack` and `removeScript` 45 | - readme 46 | - removed ES6 version 47 | 48 | ## 0.0.0 49 | 50 | - init version 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tomek Fijoł 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: ['**'] 4 | # branches: [master, feat/**, fix/**] 5 | # branches: [feat/**, fix/**] 6 | # pull_request: 7 | # branches: [master] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | check-latest: true 19 | cache: 'npm' 20 | - run: npm ci 21 | - run: npm run build 22 | lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | check-latest: true 30 | cache: 'npm' 31 | - run: npm ci 32 | - run: npm run lint 33 | types: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-node@v4 38 | with: 39 | node-version: 20 40 | check-latest: true 41 | cache: 'npm' 42 | - run: npm ci 43 | - run: npm run check-types 44 | prettier: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: actions/setup-node@v4 49 | with: 50 | node-version: 20 51 | check-latest: true 52 | cache: 'npm' 53 | - run: npm ci 54 | - run: npm run format-check 55 | test: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | - uses: actions/setup-node@v4 60 | with: 61 | node-version: 20 62 | check-latest: true 63 | cache: 'npm' 64 | - run: npm ci 65 | - run: npx playwright install chromium 66 | - run: npm run test:run 67 | -------------------------------------------------------------------------------- /test/removeScript.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, expect, test } from 'vitest'; 2 | import { preview } from 'vite'; 3 | import type { PreviewServer } from 'vite'; 4 | import { Browser, Page, chromium } from 'playwright'; 5 | import { TIMEOUT } from './constants'; 6 | 7 | let browser: Browser; 8 | let server: PreviewServer; 9 | let page: Page; 10 | 11 | beforeAll(async () => { 12 | browser = await chromium.launch({ headless: true }); 13 | server = await preview({ preview: { port: 3004 } }); 14 | page = await browser.newPage(); 15 | }); 16 | 17 | afterAll(async () => { 18 | await browser.close(); 19 | await new Promise((resolve, reject) => { 20 | server.httpServer.close((error: unknown) => 21 | error ? reject(error) : resolve(), 22 | ); 23 | }); 24 | }); 25 | 26 | test( 27 | 'removeScript true', 28 | async () => { 29 | try { 30 | await page.goto('http://localhost:3004'); 31 | await page.evaluate(async () => { 32 | await window.simpleLoadScript({ 33 | url: '//code.jquery.com/jquery-2.2.3.js', 34 | removeScript: true, 35 | attrs: { id: 'jquery' }, 36 | }); 37 | }); 38 | const jquery = await page.$('script#jquery'); 39 | const id = await page.evaluate((script) => script?.id, jquery); 40 | const nodeType = await page.evaluate( 41 | (script) => script?.nodeType, 42 | jquery, 43 | ); 44 | expect(id).toBeUndefined(); 45 | expect(nodeType).toBeUndefined(); 46 | } catch (err) { 47 | expect(err).toBeUndefined(); 48 | } 49 | }, 50 | TIMEOUT, 51 | ); 52 | 53 | test( 54 | 'removeScript false', 55 | async () => { 56 | try { 57 | await page.goto('http://localhost:3004'); 58 | await page.evaluate(async () => { 59 | await window.simpleLoadScript({ 60 | url: '//code.jquery.com/jquery-2.2.3.js', 61 | attrs: { id: 'jquery' }, 62 | }); 63 | }); 64 | const jquery = await page.$('script#jquery'); 65 | const id = await page.evaluate((script) => script?.id, jquery); 66 | expect(id).toBe('jquery'); 67 | } catch (err) { 68 | expect(err).toBeUndefined(); 69 | } 70 | }, 71 | TIMEOUT, 72 | ); 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-load-script", 3 | "version": "2.1.1", 4 | "description": "Very simple promise based script and JSONP", 5 | "repository": "tomek-f/simple-load-script", 6 | "keywords": [ 7 | "script", 8 | "promise", 9 | "JSONP" 10 | ], 11 | "type": "module", 12 | "scripts": { 13 | "check-all": "npm run lint && npm run format-check && npm run check-types && npm run test:run", 14 | "prepublishOnly": "npm run check-all && npm run build", 15 | "build": "rollup --c ./rollup.config.ts --configPlugin typescript", 16 | "watch": "npm run build -- -w", 17 | "lint": "NODE_ENV=production oxlint --import-plugin --promise-plugin --node-plugin ./", 18 | "vite:build-and-preview": "vite build && vite preview", 19 | "test:run": "vite build && vitest run --reporter=verbose", 20 | "test:ui": "vite build && vitest --reporter=verbose --ui", 21 | "test:watch": "vite build && vitest --reporter=verbose", 22 | "format-check": "prettier --check ./", 23 | "format-write": "prettier --write ./", 24 | "check-types": "echo 'checking types…' && tsc && echo '…no type problems'" 25 | }, 26 | "files": [ 27 | "src", 28 | "dist", 29 | "changelog.md" 30 | ], 31 | "main": "./dist/index.umd.js", 32 | "umd:main": "./dist/index.umd.js", 33 | "module": "./dist/index.es.js", 34 | "types": "./dist/index.d.ts", 35 | "exports": { 36 | "./package.json": "./package.json", 37 | ".": { 38 | "types": "./dist/index.d.ts", 39 | "import": "./dist/index.es.js", 40 | "require": "./dist/index.cjs.js", 41 | "default": "./dist/index.umd.js" 42 | } 43 | }, 44 | "author": "Tomek Fijoł (http://tomekf.pl/)", 45 | "contributors": [ 46 | { 47 | "name": "Martin Jurča", 48 | "email": "martin.jurca@firma.seznam.cz" 49 | }, 50 | { 51 | "name": "Tom Conroy", 52 | "url": "https://github.com/tconroy" 53 | } 54 | ], 55 | "license": "MIT", 56 | "readmeFilename": "README.md", 57 | "devDependencies": { 58 | "@rollup/plugin-terser": "^0.4.4", 59 | "@rollup/plugin-typescript": "^12.1.2", 60 | "@vitest/ui": "^3.1.1", 61 | "oxlint": "^0.16.5", 62 | "playwright": "^1.51.1", 63 | "prettier": "^3.5.3", 64 | "rollup": "^4.40.0", 65 | "tslib": "^2.8.1", 66 | "typescript": "^5.8.3", 67 | "vite": "^6.2.0", 68 | "vitest": "^3.1.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/attrs.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, expect, test } from 'vitest'; 2 | import { preview } from 'vite'; 3 | import type { PreviewServer } from 'vite'; 4 | import { Browser, Page, chromium } from 'playwright'; 5 | import { TIMEOUT } from './constants'; 6 | 7 | let browser: Browser; 8 | let server: PreviewServer; 9 | let page: Page; 10 | 11 | beforeAll(async () => { 12 | browser = await chromium.launch({ headless: true }); 13 | server = await preview({ preview: { port: 3001 } }); 14 | page = await browser.newPage(); 15 | }); 16 | 17 | afterAll(async () => { 18 | await browser.close(); 19 | await new Promise((resolve, reject) => { 20 | server.httpServer.close((error: unknown) => 21 | error ? reject(error) : resolve(), 22 | ); 23 | }); 24 | }); 25 | 26 | test( 27 | 'add attrs', 28 | async () => { 29 | try { 30 | await page.goto('http://localhost:3001'); 31 | await page.evaluate(async () => { 32 | await window.simpleLoadScript({ 33 | url: '//code.jquery.com/jquery-2.2.3.js', 34 | attrs: { id: 'jquery', 'data-test': 'test' }, 35 | }); 36 | }); 37 | const jquery = await page.$('script#jquery'); 38 | const id = await page.evaluate((script) => script?.id, jquery); 39 | const dataTest = await page.evaluate( 40 | (script) => script?.dataset.test, 41 | jquery, 42 | ); 43 | 44 | expect(id).toBe('jquery'); 45 | expect(dataTest).toBe('test'); 46 | } catch (err) { 47 | expect(err).toBeUndefined(); 48 | } 49 | }, 50 | TIMEOUT, 51 | ); 52 | 53 | test( 54 | 'do not add attrs', 55 | async () => { 56 | try { 57 | await page.goto('http://localhost:3001'); 58 | await page.evaluate(async () => { 59 | await window.simpleLoadScript({ 60 | url: '//code.jquery.com/jquery-2.2.3.js', 61 | }); 62 | }); 63 | const jquery = await page.$('script'); 64 | const jqueryWithId = await page.$('script#jquery'); 65 | const nodeType = await page.evaluate( 66 | (script) => script?.nodeType, 67 | jquery, 68 | ); 69 | const nodeTypeWithId = await page.evaluate( 70 | (script) => script?.nodeType, 71 | jqueryWithId, 72 | ); 73 | 74 | expect(nodeType).toBe(1); 75 | expect(nodeTypeWithId).toBeUndefined(); 76 | } catch (err) { 77 | expect(err).toBeUndefined(); 78 | } 79 | }, 80 | TIMEOUT, 81 | ); 82 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | url: string; 3 | attrs?: Record; 4 | inBody?: boolean; 5 | insertInto?: string | null; 6 | removeScript?: boolean; 7 | } 8 | 9 | const defaultConfig = { 10 | url: '', 11 | attrs: {}, 12 | inBody: false, 13 | insertInto: null, 14 | removeScript: false, 15 | } satisfies Config; 16 | 17 | export default function simpleLoadScript( 18 | config: (Config | string)[], 19 | ): Promise<(HTMLScriptElement | undefined)[]>; 20 | export default function simpleLoadScript( 21 | config: Config | string, 22 | ): Promise; 23 | export default function simpleLoadScript( 24 | config: Config | string | (Config | string)[], 25 | ): Promise { 26 | if (Array.isArray(config)) { 27 | return Promise.all(config.map(simpleLoadScript)); 28 | } 29 | 30 | return new Promise((resolve, reject) => { 31 | if ( 32 | !( 33 | (typeof config === 'object' && config.url) || 34 | typeof config === 'string' 35 | ) 36 | ) { 37 | console.log({ config }); 38 | reject(new Error('Object with url or url string needed')); 39 | return; 40 | } 41 | 42 | const configProcessed: Required = Object.assign( 43 | {}, 44 | defaultConfig, 45 | typeof config === 'string' ? { url: config } : config, 46 | ); 47 | const { url, attrs, inBody, insertInto, removeScript } = 48 | configProcessed; 49 | const script = document.createElement('script'); 50 | const where: HTMLElement | null = insertInto 51 | ? document.querySelector(insertInto) 52 | : inBody 53 | ? document.body 54 | : document.head; 55 | 56 | if (attrs && typeof attrs === 'object') { 57 | for (const attr of Object.keys(attrs)) { 58 | script.setAttribute(attr, attrs[attr]); 59 | } 60 | } 61 | 62 | if (where == null) { 63 | reject(new Error('No DOM element to append script')); 64 | return; 65 | } 66 | 67 | script.addEventListener('load', () => { 68 | if (removeScript) { 69 | where.removeChild(script); 70 | } 71 | resolve(removeScript ? undefined : script); 72 | }); 73 | script.addEventListener('error', (/* err */) => { 74 | if (removeScript) { 75 | where.removeChild(script); 76 | } 77 | // TODO ? just return err 78 | // TODO ? re-throw err with changed message 79 | reject(new Error('Loading script error')); 80 | }); 81 | script.src = url; 82 | where.appendChild(script); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /test/array.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, expect, test } from 'vitest'; 2 | import { preview } from 'vite'; 3 | import type { PreviewServer } from 'vite'; 4 | import { Browser, Page, chromium } from 'playwright'; 5 | import { TIMEOUT } from './constants'; 6 | 7 | let browser: Browser; 8 | let server: PreviewServer; 9 | let page: Page; 10 | 11 | beforeAll(async () => { 12 | browser = await chromium.launch({ headless: true }); 13 | server = await preview({ preview: { port: 3005 } }); 14 | page = await browser.newPage(); 15 | }); 16 | 17 | afterAll(async () => { 18 | await browser.close(); 19 | await new Promise((resolve, reject) => { 20 | server.httpServer.close((error: unknown) => 21 | error ? reject(error) : resolve(), 22 | ); 23 | }); 24 | }); 25 | 26 | test( 27 | 'load array ok', 28 | async () => { 29 | try { 30 | await page.goto('http://localhost:3005'); 31 | const scriptRefs = await page.evaluate(async () => { 32 | const [a, b, c] = await window.simpleLoadScript([ 33 | '//code.jquery.com/jquery-2.2.3.js', 34 | { 35 | url: '//code.jquery.com/jquery-2.2.2.js', 36 | attrs: { id: 'jquery2' }, 37 | }, 38 | { 39 | url: '//code.jquery.com/jquery-2.2.1.js', 40 | attrs: { id: 'jquery3' }, 41 | }, 42 | ]); 43 | return [a, b, c]; 44 | }); 45 | expect(scriptRefs.length).toBe(3); 46 | const jquery1 = await page.$( 47 | 'head script[src="//code.jquery.com/jquery-2.2.3.js"]', 48 | ); 49 | const jquery2 = await page.$('script#jquery2'); 50 | const jquery3 = await page.$('script#jquery3'); 51 | const src1 = await page.evaluate( 52 | (script) => script?.getAttribute('src'), 53 | jquery1, 54 | ); 55 | const id2 = await page.evaluate((script) => script?.id, jquery2); 56 | const id3 = await page.evaluate((script) => script?.id, jquery3); 57 | expect(src1).toBe('//code.jquery.com/jquery-2.2.3.js'); 58 | expect(id2).toBe('jquery2'); 59 | expect(id3).toBe('jquery3'); 60 | } catch (err) { 61 | expect(err).toBeUndefined(); 62 | } 63 | }, 64 | TIMEOUT, 65 | ); 66 | 67 | test( 68 | 'load array error', 69 | async () => { 70 | try { 71 | await page.goto('http://localhost:3005'); 72 | await page.evaluate(async () => { 73 | await window.simpleLoadScript([ 74 | '//wrong.domain/jquery-2.2.3.js', 75 | { 76 | url: '//code.jquery.com/jquery-2.2.2.js', 77 | attrs: { id: 'jquery2' }, 78 | }, 79 | { 80 | url: '//code.jquery.com/jquery-2.2.1.js', 81 | attrs: { id: 'jquery3' }, 82 | }, 83 | ]); 84 | }); 85 | } catch (err) { 86 | expect( 87 | (err as Error).message.includes('Error: Loading script error'), 88 | ).toBe(true); 89 | } 90 | }, 91 | TIMEOUT, 92 | ); 93 | -------------------------------------------------------------------------------- /test/placement.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, expect, test } from 'vitest'; 2 | import { preview } from 'vite'; 3 | import type { PreviewServer } from 'vite'; 4 | import { Browser, Page, chromium } from 'playwright'; 5 | import { TIMEOUT } from './constants'; 6 | 7 | let browser: Browser; 8 | let server: PreviewServer; 9 | let page: Page; 10 | 11 | beforeAll(async () => { 12 | browser = await chromium.launch({ headless: true }); 13 | server = await preview({ preview: { port: 3003 } }); 14 | page = await browser.newPage(); 15 | }); 16 | 17 | afterAll(async () => { 18 | await browser.close(); 19 | await new Promise((resolve, reject) => { 20 | server.httpServer.close((error: unknown) => 21 | error ? reject(error) : resolve(), 22 | ); 23 | }); 24 | }); 25 | 26 | test( 27 | 'placement head', 28 | async () => { 29 | try { 30 | await page.goto('http://localhost:3003'); 31 | await page.evaluate(async () => { 32 | await window.simpleLoadScript({ 33 | url: '//code.jquery.com/jquery-2.2.3.js', 34 | attrs: { id: 'jquery' }, 35 | }); 36 | }); 37 | const jquery = await page.$('head script#jquery'); 38 | const id = await page.evaluate((script) => script?.id, jquery); 39 | expect(id).toBe('jquery'); 40 | } catch (err) { 41 | expect(err).toBeUndefined(); 42 | } 43 | }, 44 | TIMEOUT, 45 | ); 46 | 47 | test( 48 | 'placement body', 49 | async () => { 50 | try { 51 | await page.goto('http://localhost:3003'); 52 | await page.evaluate(async () => { 53 | await window.simpleLoadScript({ 54 | url: '//code.jquery.com/jquery-2.2.3.js', 55 | attrs: { id: 'jquery' }, 56 | inBody: true, 57 | }); 58 | }); 59 | const jquery = await page.$('body script#jquery'); 60 | const id = await page.evaluate((script) => script?.id, jquery); 61 | expect(id).toBe('jquery'); 62 | } catch (err) { 63 | expect(err).toBeUndefined(); 64 | } 65 | }, 66 | TIMEOUT, 67 | ); 68 | 69 | test( 70 | 'insertInto ok', 71 | async () => { 72 | try { 73 | await page.goto('http://localhost:3003'); 74 | await page.evaluate(async () => { 75 | await window.simpleLoadScript({ 76 | url: '//code.jquery.com/jquery-2.2.3.js', 77 | attrs: { id: 'jquery' }, 78 | insertInto: '#insert', 79 | }); 80 | }); 81 | const jquery = await page.$('#insert script#jquery'); 82 | const id = await page.evaluate((script) => script?.id, jquery); 83 | expect(id).toBe('jquery'); 84 | } catch (err) { 85 | expect(err).toBeUndefined(); 86 | } 87 | }, 88 | TIMEOUT, 89 | ); 90 | 91 | test( 92 | 'insertInto error', 93 | async () => { 94 | try { 95 | await page.goto('http://localhost:3003'); 96 | await page.evaluate(async () => { 97 | await window.simpleLoadScript({ 98 | url: '//code.jquery.com/jquery-2.2.3.js', 99 | attrs: { id: 'jquery' }, 100 | insertInto: '#insert1', 101 | }); 102 | }); 103 | } catch (err) { 104 | expect( 105 | (err as Error).message.includes( 106 | 'No DOM element to append script', 107 | ), 108 | ).toBe(true); 109 | } 110 | }, 111 | TIMEOUT, 112 | ); 113 | -------------------------------------------------------------------------------- /test/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, test } from 'vitest'; 2 | import { preview } from 'vite'; 3 | import type { PreviewServer } from 'vite'; 4 | import { Browser, Page, chromium } from 'playwright'; 5 | import { TIMEOUT } from './constants'; 6 | 7 | // https://github.com/vitest-dev/vitest/blob/main/examples/puppeteer/test/basic.test.ts 8 | // https://gist.github.com/mizchi/5f67109d0719ef6dd57695e1f528ce8d 9 | 10 | let browser: Browser; 11 | let server: PreviewServer; 12 | let page: Page; 13 | 14 | beforeAll(async () => { 15 | browser = await chromium.launch({ headless: true }); 16 | server = await preview({ preview: { port: 3000 } }); 17 | page = await browser.newPage(); 18 | }); 19 | 20 | afterAll(async () => { 21 | await browser.close(); 22 | await new Promise((resolve, reject) => { 23 | server.httpServer.close((error: unknown) => 24 | error ? reject(error) : resolve(), 25 | ); 26 | }); 27 | }); 28 | 29 | test( 30 | 'load url ok', 31 | async () => { 32 | try { 33 | await page.goto('http://localhost:3000'); 34 | const ref = await page.evaluate(async () => { 35 | const scriptRef = await window.simpleLoadScript( 36 | '//code.jquery.com/jquery-2.2.3.js', 37 | ); 38 | return scriptRef; 39 | }); 40 | expect(ref).toBeDefined(); 41 | } catch (err) { 42 | expect(err).toBeUndefined(); 43 | } 44 | }, 45 | TIMEOUT, 46 | ); 47 | 48 | test( 49 | 'load config ok', 50 | async () => { 51 | try { 52 | await page.goto('http://localhost:3000'); 53 | const ref = await page.evaluate(async () => { 54 | const scriptRef = await window.simpleLoadScript({ 55 | url: '//code.jquery.com/jquery-2.2.3.js', 56 | }); 57 | return scriptRef; 58 | }); 59 | expect(ref).toBeDefined(); 60 | } catch (err) { 61 | expect(err).toBeUndefined(); 62 | } 63 | }, 64 | TIMEOUT, 65 | ); 66 | 67 | test( 68 | 'wrong url error', 69 | async () => { 70 | try { 71 | await page.goto('http://localhost:3000'); 72 | await page.evaluate(async () => { 73 | await window.simpleLoadScript('//wrong.domain/jquery-2.2.3.js'); 74 | }); 75 | } catch (err) { 76 | expect( 77 | (err as Error).message.includes('Error: Loading script error'), 78 | ).toBe(true); 79 | } 80 | }, 81 | TIMEOUT, 82 | ); 83 | 84 | describe('wrong config error', () => { 85 | test( 86 | 'no param', 87 | async () => { 88 | try { 89 | await page.goto('http://localhost:3000'); 90 | await page.evaluate(async () => { 91 | // @ts-expect-error Testing wrong config 92 | await window.simpleLoadScript(); 93 | }); 94 | } catch (err) { 95 | expect( 96 | (err as Error).message.includes( 97 | 'Error: Object with url or url string needed', 98 | ), 99 | ).toBe(true); 100 | } 101 | }, 102 | TIMEOUT, 103 | ); 104 | test( 105 | 'bad config', 106 | async () => { 107 | try { 108 | await page.goto('http://localhost:3000'); 109 | await page.evaluate(async () => { 110 | await window.simpleLoadScript({ 111 | // @ts-expect-error Testing wrong config 112 | elo: '//code.jquery.com/jquery-2.2.3.js', 113 | }); 114 | }); 115 | } catch (err) { 116 | expect( 117 | (err as Error).message.includes( 118 | 'Error: Object with url or url string needed', 119 | ), 120 | ).toBe(true); 121 | } 122 | }, 123 | TIMEOUT, 124 | ); 125 | }); 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-load-script 2 | 3 | > Very simple promise based script loader and JSONP 4 | 5 | [![.github/workflows/check.yml](https://github.com/tomek-f/simple-load-script/actions/workflows/check.yml/badge.svg)](https://github.com/tomek-f/simple-load-script/actions/workflows/check.yml) 6 | 7 | ## Usage 8 | 9 | ### Async/await 10 | 11 | ```js 12 | import simpleLoadScript from 'simple-load-script'; 13 | 14 | try { 15 | const scriptRef = await simpleLoadScript( 16 | 'https://code.jquery.com/jquery-3.7.1.min.js', 17 | ); 18 | 19 | console.log(scriptRef); // HTMLScriptElement 20 | } catch (err) { 21 | console.log(err); 22 | } 23 | ``` 24 | 25 | ### Promise 26 | 27 | ```js 28 | import simpleLoadScript from 'simple-load-script'; 29 | 30 | simpleLoadScript('https://code.jquery.com/jquery-3.7.1.min.js') 31 | .then(function (scriptRef) { 32 | console.log(scriptRef); // HTMLScriptElement 33 | }) 34 | .catch(function (err) { 35 | console.log(err); 36 | }); 37 | ``` 38 | 39 | ### Config object 40 | 41 | ```js 42 | import simpleLoadScript from 'simple-load-script'; 43 | 44 | try { 45 | const scriptRef = await simpleLoadScript({ 46 | url: 'https://code.jquery.com/jquery-3.7.1.min.js', 47 | inBody: true, 48 | attrs: { id: 'one', charset: 'UTF-8' }, 49 | }); 50 | 51 | console.log(scriptRef); // HTMLScriptElement inBody with attrs present 52 | } catch (err) { 53 | console.log(err); 54 | } 55 | ``` 56 | 57 | ### Google Maps API 58 | 59 | Runs global callback (window.gmapiready) 60 | 61 | ```js 62 | import simpleLoadScript from 'simple-load-script'; 63 | 64 | try { 65 | const scriptRef = await simpleLoadScript( 66 | '//maps.googleapis.com/maps/api/js?&callback=gmapiready', 67 | ); 68 | 69 | console.log(scriptRef); // HTMLScriptElement 70 | } catch (err) { 71 | console.log(err); 72 | } 73 | ``` 74 | 75 | ### JSONP 76 | 77 | Runs global callback (window.elo) 78 | 79 | ```js 80 | var simpleLoadScript = require('simple-load-script'); 81 | 82 | try { 83 | const scriptRef = await simpleLoadScript({ 84 | url: '//api.ipinfodb.com/v3/ip-city/?format=json&callback=elo', 85 | removeScript: true, 86 | }); 87 | 88 | console.log(scriptRef); // undefined 89 | } catch (err) { 90 | console.log(err); 91 | } 92 | ``` 93 | 94 | ### Array mode - objects and urls, callBackNames must have unique names 95 | 96 | ```js 97 | import simpleLoadScript from 'simple-load-script'; 98 | 99 | try { 100 | const scriptRefs = await simpleLoadScript([ 101 | '//maps.googleapis.com/maps/api/js?&callback=gmapiready', 102 | { 103 | url: '//api.ipinfodb.com/v3/ip-city/?format=json&callback=elo', 104 | removeScript: true, 105 | }, 106 | 'https://code.jquery.com/jquery-3.7.1.min.js', 107 | ]); 108 | 109 | console.log(scriptRefs); // HTMLScriptElement[] 110 | } catch (err) { 111 | console.log(err); 112 | } 113 | ``` 114 | 115 | ## Arguments 116 | 117 | `Config | string | (Config | string)[]` 118 | 119 | ## Config 120 | 121 | ### Interface 122 | 123 | ```ts 124 | interface Config { 125 | url: string; 126 | attrs?: Record; 127 | inBody?: boolean; 128 | insertInto?: string | null; 129 | removeScript?: boolean; 130 | } 131 | ``` 132 | 133 | ### Default values 134 | 135 | ```js 136 | const defaultConfig = { 137 | url: '', 138 | attrs: {}, 139 | inBody: false, 140 | insertInto: null, 141 | removeScript: false, 142 | }; 143 | ``` 144 | 145 | - `url` - file to append to body 146 | - `attrs` - with attributes to append to script tag (`charset`, `type`, `id`, …) 147 | - `inBody` - append to `document.body` instead of `document.head` 148 | - `insertInto` - [CSS selector (an ID, class name, element name, etc.)](https://developer.mozilla.org/en/docs/Web/API/Document/querySelector) to insert the script into. Overrides `inBody` value. 149 | - `removeScript` - remove script tag after load; it's always removed on an error 150 | 151 | ## Specific import 152 | 153 | [Check files](https://www.npmjs.com/package/simple-load-script?activeTab=code) or package.json 154 | 155 | ## Changelog 156 | 157 | [View on github](https://github.com/tomek-f/simple-load-script/blob/master/changelog.md). 158 | 159 | ## Misc. 160 | 161 | - uses addEventListener, Array.isArray, for…of, destructuring Promise & Promise.all 162 | --------------------------------------------------------------------------------