Skip to content

Commit 492de64

Browse files
committed
feat: adds loadScript
1 parent 24b056d commit 492de64

File tree

10 files changed

+386
-148
lines changed

10 files changed

+386
-148
lines changed

README.md

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,73 @@
1-
<p>
2-
<img width="100%" src="https://assets.solidjs.com/banner?type=Ecosystem&background=tiles&project=library-name" alt="solid-create-script">
3-
</p>
1+
[![NPM Version](https://img.shields.io/npm/v/load-script.svg?style=for-the-badge)](https://www.npmjs.com/package/load-script) [![Build Status](https://img.shields.io/github/actions/workflow/status/dsnchz/load-script/ci.yaml?branch=main&logo=github&style=for-the-badge)](https://github.com/dsnchz/load-script/actions/workflows/ci.yaml) [![bun](https://img.shields.io/badge/maintained%20with-bun-cc00ff.svg?style=for-the-badge&logo=bun)](https://bun.sh/)
42

5-
# Template: SolidJS Library
3+
# @dschz/load-script
64

7-
Template for [SolidJS](https://www.solidjs.com/) library package. Bundling of the library is managed by [tsup](https://tsup.egoist.dev/).
5+
Utility function to dynamically load external scripts in both declarative and imperative styles within SolidJS.
86

9-
Other things configured include:
7+
## Installation
108

11-
- Bun (for dependency management and running scripts)
12-
- TypeScript
13-
- ESLint / Prettier
14-
- Solid Testing Library + Vitest (for testing)
15-
- Playground app using library
16-
- GitHub Actions (for all CI/CD)
9+
```bash
10+
npm install @dschz/load-script
11+
pnpm install @dschz/load-script
12+
yarn install @dschz/load-script
13+
bun install @dschz/load-script
14+
```
1715

18-
## Getting Started
16+
## Summary
1917

20-
Some pre-requisites before install dependencies:
18+
```ts
19+
import { loadScript } from "@dschz/load-script";
20+
```
2121

22-
- Install Node Version Manager (NVM)
23-
```bash
24-
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
25-
```
26-
- Install Bun
27-
```bash
28-
curl -fsSL https://bun.sh/install | bash
29-
```
22+
## API Breakdown
3023

31-
### Installing Dependencies
24+
### `loadScript`
3225

33-
```bash
34-
nvm use
35-
bun install
36-
```
26+
Utility function that returns a `Promise<HTMLScriptElement>`.
3727

38-
### Local Development Build
28+
The interface signature is as follows:
3929

40-
```bash
41-
bun start
30+
```ts
31+
export const loadScript = (src: string, props?: HTMLScriptElementProps, target?: HTMLElement)
4232
```
4333

44-
### Linting & Formatting
34+
The three arguments:
4535

46-
```bash
47-
bun run lint # checks source for lint violations
48-
bun run format # checks source for format violations
36+
- `src`: the script source to download
37+
- `props`: any `<script>` specific instance properties to apply
38+
- `target`: the DOM target to append the `<script>` tag to.
4939

50-
bun run lint:fix # fixes lint violations
51-
bun run format:fix # fixes format violations
40+
Useful for:
41+
42+
- Full control over script placement
43+
- Timing-sensitive insertions
44+
45+
```ts
46+
import { Switch, Match, onMount } from "solid-js"
47+
import { loadScript } from "@dschz/load-script"
48+
49+
const CustomComponent = () => {
50+
let containerRef!: HTMLElement;
51+
52+
onMount(async () => {
53+
const script = await loadScript(
54+
"https://example.com/widget.js",
55+
{ type: "text/javascript" },
56+
containerRef,
57+
);
58+
});
59+
60+
return (
61+
<div ref={containerRef} />
62+
)
63+
}
5264
```
5365

54-
### Contributing
66+
## Notes
67+
68+
- Scripts are automatically cached to prevent duplication.
69+
- The script is not removed on cleanup/unmount.
5570

56-
The only requirements when contributing are:
71+
## Feedback
5772

58-
- You keep a clean git history in your branch
59-
- rebasing `main` instead of making merge commits.
60-
- Using proper commit message formats that adhere to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
61-
- Additionally, squashing (via rebase) commits that are not [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
62-
- CI checks pass before merging into `main`
73+
Feel free to post issues or suggestions to help improve this library.

bun.lock

Lines changed: 2 additions & 56 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jsr.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"name": "template-solidjs-library",
3-
"version": "0.0.0",
2+
"name": "@dschz/load-script",
3+
"version": "0.1.0",
44
"license": "MIT",
55
"exports": "./src/index.tsx"
66
}

package.json

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
2-
"name": "template-solidjs-library",
3-
"version": "0.0.0",
4-
"description": "Template for SolidJS library using tsup for bundling. Configured with Bun, NVM, TypeScript, ESLint, Prettier, Vitest, and GHA",
2+
"name": "@dschz/load-script",
3+
"version": "0.1.0",
4+
"description": "Dynamically load scripts in the browser with caching.",
55
"type": "module",
66
"author": "Daniel Sanchez <dsanc89@icloud.com>",
77
"license": "MIT",
8-
"homepage": "https://github.com/thedanchez/template-solidjs-library#readme",
8+
"homepage": "https://github.com/dsnchz/load-script#readme",
99
"bugs": {
10-
"url": "https://github.com/thedanchez/template-solidjs-library/issues"
10+
"url": "https://github.com/dsnchz/load-script/issues"
1111
},
1212
"files": [
1313
"dist"
@@ -43,9 +43,7 @@
4343
},
4444
"devDependencies": {
4545
"@changesets/cli": "^2.29.3",
46-
"@solidjs/testing-library": "^0.8.10",
4746
"@tailwindcss/vite": "^4.1.5",
48-
"@testing-library/jest-dom": "^6.6.3",
4947
"@types/bun": "^1.2.12",
5048
"@typescript-eslint/eslint-plugin": "^8.32.0",
5149
"@typescript-eslint/parser": "^8.32.0",
@@ -59,14 +57,10 @@
5957
"prettier": "^3.5.3",
6058
"tailwindcss": "^4.1.5",
6159
"tsup": "^8.4.0",
62-
"tsup-preset-solid": "^2.2.0",
6360
"typescript": "^5.8.3",
6461
"typescript-eslint": "^8.32.0",
6562
"vite": "^6.3.5",
6663
"vite-plugin-solid": "^2.11.6",
6764
"vitest": "^3.1.3"
68-
},
69-
"peerDependencies": {
70-
"solid-js": ">=1.6.0"
7165
}
7266
}

src/__tests__/loadScript.test.tsx

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { beforeEach, describe, expect, test, vi } from "vitest";
2+
3+
import { __resetScriptCache, type HTMLScriptElementProps, loadScript } from "../index";
4+
5+
const SCRIPT_SRC = "https://example.com/script.js";
6+
7+
describe("loadScript", () => {
8+
beforeEach(() => {
9+
document.querySelectorAll("script").forEach((s) => s.remove());
10+
__resetScriptCache();
11+
});
12+
13+
test("rejects if src is not provided", async () => {
14+
await expect(loadScript("")).rejects.toThrow('No "src" provided for createScript');
15+
});
16+
17+
test("resolves immediately if script already exists in DOM", async () => {
18+
const script = document.createElement("script");
19+
script.src = SCRIPT_SRC;
20+
document.head.appendChild(script);
21+
22+
const resolved = await loadScript(SCRIPT_SRC);
23+
expect(resolved).toBe(script);
24+
});
25+
26+
test("injects script only once per src when innerHTML is not set", async () => {
27+
const promise1 = loadScript(SCRIPT_SRC, { async: true });
28+
const promise2 = loadScript(SCRIPT_SRC, { async: true });
29+
30+
const scripts = document.querySelectorAll(`script[src='${SCRIPT_SRC}']`);
31+
expect(scripts.length).toBe(1);
32+
33+
scripts[0]?.dispatchEvent(new Event("load"));
34+
35+
const s1 = await promise1;
36+
const s2 = await promise2;
37+
38+
expect(s1).toBe(s2);
39+
});
40+
41+
test("injects script multiple times when different innerHTML is set", async () => {
42+
const promise1 = loadScript(SCRIPT_SRC, {
43+
innerHTML: `{ "symbol": "AAPL" }`,
44+
});
45+
const promise2 = loadScript(SCRIPT_SRC, {
46+
innerHTML: `{ "symbol": "TSLA" }`,
47+
});
48+
49+
const scripts = document.querySelectorAll(`script[src='${SCRIPT_SRC}']`);
50+
expect(scripts.length).toBe(2);
51+
52+
scripts[0]?.dispatchEvent(new Event("load"));
53+
scripts[1]?.dispatchEvent(new Event("load"));
54+
55+
const s1 = await promise1;
56+
const s2 = await promise2;
57+
58+
expect(s1).not.toBe(s2);
59+
});
60+
61+
test("assigns all direct script properties correctly", async () => {
62+
const props = {
63+
async: false,
64+
defer: true,
65+
fetchPriority: "low",
66+
noModule: true,
67+
type: "text/javascript",
68+
crossOrigin: "anonymous",
69+
referrerPolicy: "origin",
70+
integrity: "sha384-abc123",
71+
nonce: "xyz123",
72+
textContent: "Hello, world!",
73+
} as HTMLScriptElementProps;
74+
75+
const promise = loadScript(SCRIPT_SRC, props);
76+
77+
const [script] = document.querySelectorAll(`script[src="${SCRIPT_SRC}"]`);
78+
expect(script).toBeDefined();
79+
80+
script?.dispatchEvent(new Event("load"));
81+
const resolved = await promise;
82+
83+
expect(resolved.async).toBe(false);
84+
expect(resolved.defer).toBe(true);
85+
expect(resolved.fetchPriority).toBe("low");
86+
expect(resolved.noModule).toBe(true);
87+
expect(resolved.type).toBe("text/javascript");
88+
expect(resolved.crossOrigin).toBe("anonymous");
89+
expect(resolved.referrerPolicy).toBe("origin");
90+
expect(resolved.integrity).toBe("sha384-abc123");
91+
expect(resolved.nonce).toBe("xyz123");
92+
expect(resolved.textContent).toBe("Hello, world!");
93+
});
94+
95+
test("applies additional script attributes via setAttribute", async () => {
96+
const promise = loadScript(SCRIPT_SRC, { "data-id": "custom-script-id" });
97+
98+
const [script] = document.querySelectorAll(`script[src="${SCRIPT_SRC}"]`);
99+
expect(script).toBeDefined();
100+
101+
script?.dispatchEvent(new Event("load"));
102+
const resolved = await promise;
103+
104+
expect(resolved.getAttribute("data-id")).toBe("custom-script-id");
105+
});
106+
107+
test("resolves when script loads", async () => {
108+
const promise = loadScript(SCRIPT_SRC);
109+
const script = document.querySelector(`script[src="${SCRIPT_SRC}"]`)!;
110+
111+
script.dispatchEvent(new Event("load"));
112+
113+
const resolved = await promise;
114+
expect(resolved).toBe(script);
115+
});
116+
117+
test("rejects when script fails to load", async () => {
118+
const promise = loadScript(SCRIPT_SRC);
119+
const script = document.querySelector(`script[src="${SCRIPT_SRC}"]`)!;
120+
121+
const errorEvent = new Event("error");
122+
123+
script.dispatchEvent(errorEvent);
124+
125+
await expect(promise).rejects.toBe(errorEvent);
126+
});
127+
128+
test("calls onLoad handler when provided after script loads", async () => {
129+
const onLoad = vi.fn();
130+
131+
const promise = loadScript(SCRIPT_SRC, { onLoad });
132+
const script = document.querySelector(`script[src="${SCRIPT_SRC}"]`)!;
133+
134+
script.dispatchEvent(new Event("load"));
135+
136+
const resolved = await promise;
137+
138+
expect(onLoad).toHaveBeenCalledOnce();
139+
expect(resolved).toBe(script);
140+
});
141+
142+
test("calls onError handler when script fails to load", async () => {
143+
const onError = vi.fn();
144+
145+
const promise = loadScript(SCRIPT_SRC, { onError });
146+
147+
const script = document.querySelector(`script[src="${SCRIPT_SRC}"]`)!;
148+
149+
const errorEvent = new Event("error");
150+
151+
script.dispatchEvent(errorEvent);
152+
153+
await expect(promise).rejects.toBe(errorEvent);
154+
155+
expect(onError).toHaveBeenCalledOnce();
156+
});
157+
});

0 commit comments

Comments
 (0)