Skip to content

Commit

Permalink
feat: adds createScript hook
Browse files Browse the repository at this point in the history
  • Loading branch information
thedanchez committed Nov 22, 2024
1 parent ccd2e22 commit c40ffa8
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 61 deletions.
80 changes: 38 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,54 @@
# Template: SolidJS Library
<p>
<img width="100%" src="https://assets.solidjs.com/banner?type=Ecosystem&background=tiles&project=solid-create-script" alt="solid-create-script">
</p>

Template for [SolidJS](https://www.solidjs.com/) library package. Bundling of the library is managed by [tsup](https://tsup.egoist.dev/).
# solid-create-script

Other things configured include:
Solid utility hook to dynamically load an external script.

- Bun (for dependency management and running scripts)
- TypeScript
- ESLint / Prettier
- Solid Testing Library + Vitest (for testing)
- Playground app using library
- GitHub Actions (for all CI/CD)
### Installation

## Getting Started
```bash
npm install solid-js solid-create-script
pnpm add solid-js solid-create-script
yarn add solid-js solid-create-script
bun add solid-js solid-create-script
```

Some pre-requisites before install dependencies:
### Usage

- Install Node Version Manager (NVM)
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
```
- Install Bun
```bash
curl -fsSL https://bun.sh/install | bash
```
```tsx
const script = createScript("https://some-library.js");
const script = createScript("https://some-library.js", { async: true, ...scriptAttributes });
const script = createScript("https://some-library.js", { defer: true, ...scriptAttributes });
```

### Installing Dependencies
Under the hood `createScript` makes use of the `createResource` Solid API when loading the desired script at the specified `src`. As such, the result of `createScript` is a `Resource<Event>` object that allows you to inspect when the script has finished loading or returned an error via `script.loading` and `script.error` respectively.

```bash
nvm use
bun install
```
When using `createScript`, here are some points to be aware of:

### Local Development Build
- The created `<script>` tag will be appeneded to `<head>`.
- The created `<script>` tag will not be removed from the DOM when a component that uses this hook unmounts. (i.e. we do not make use of `onCleanup` to remove the `<script>` from `<head>`).
- The hook will ensure no duplicate `<script>` tags referencing the same `src` will be created. Moreover, when multiple components reference the same `src`, they will all point to the same shared resource ensuring consistency within the reactive graph.

```bash
bun start
```
### Full Example

### Linting & Formatting
```tsx
import { Switch, Match } from "solid-js";
import { createScript } from "solid-create-script";

```bash
bun run lint # checks source for lint violations
bun run format # checks source for format violations
function App() {
const script = createScript("https://some-library.js");

bun run lint:fix # fixes lint violations
bun run format:fix # fixes format violations
return (
<Switch fallback={<ScriptProvider>...</ScriptProvider>}>
<Match when={script.loading}>Loading Script...</Match>
<Match when={script.error}>Failed to load script: {script.error.message}</Match>
</Switch>
);
}
```

### Contributing

The only requirements when contributing are:
### Feedback

- You keep a clean git history in your branch
- rebasing `main` instead of making merge commits.
- Using proper commit message formats that adhere to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
- Additionally, squashing (via rebase) commits that are not [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
- CI checks pass before merging into `main`
Feel free to post any issues or suggestions to help improve this utility hook.
22 changes: 17 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
{
"name": "template-solidjs-library",
"version": "0.0.0",
"description": "Template for SolidJS library using tsup for bundling. Configured with Bun, NVM, TypeScript, ESLint, Prettier, Vitest, and GHA",
"name": "solid-create-script",
"version": "1.0.0",
"description": "Solid utility hook to dynamically load an external script.",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"author": "Daniel Sanchez <[email protected]>",
"license": "MIT",
"homepage": "https://github.com/thedanchez/template-solidjs-library#readme",
"homepage": "https://github.com/thedanchez/solid-create-script#readme",
"bugs": {
"url": "https://github.com/thedanchez/template-solidjs-library/issues"
"url": "https://github.com/thedanchez/solid-create-script/issues"
},
"exports": {
"solid": "./dist/index.jsx",
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"browser": {},
"typesVersions": {},
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
Expand Down
15 changes: 4 additions & 11 deletions playground/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { createSignal } from "solid-js";
import { createScript } from "../src";

export const App = () => {
const [count, setCount] = createSignal(0);
const script = createScript("https://cdn.plaid.com/link/v2/stable/link-initialize.js", { defer: true });

return (
<div>
<div>Playground App</div>
<div>Count: {count()}</div>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}
>
Increment Count
</button>
<div>Playground: solid-create-script</div>
<div>Script Loading: {script.loading.toString()}</div>
</div>
);
};
68 changes: 68 additions & 0 deletions src/createScript.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createResource, type JSX, splitProps } from "solid-js";
import type { DOMElement } from "solid-js/jsx-runtime";

type ScriptAttributes = Omit<JSX.ScriptHTMLAttributes<HTMLScriptElement>, "src">;

type ScriptEvent = Event & {
currentTarget: HTMLScriptElement;
target: DOMElement;
};

// Promise cache for script sources
const SCRIPT_PROMISES = new Map<string, Promise<Event>>();

const loadScript = async (src: string, attributes: Readonly<ScriptAttributes>) => {
const [initEvents, otherAttributes] = splitProps(attributes, ["onload", "onLoad", "onerror", "onError"]);

// 1. Reject if no src provided
if (!src) return Promise.reject(new Error('No "src" provided for createScript'));

// 2. Return cached promise if referencing same src
if (SCRIPT_PROMISES.has(src)) return SCRIPT_PROMISES.get(src)!;

// 3. Check if script already exists (may have been added externally not via this hook)
if (document.querySelector(`script[src="${src}"]`)) return Promise.resolve(new Event("already-loaded"));

// 4. Create new script element
const promise = new Promise<Event>((resolve, reject) => {
const script = document.createElement("script");
script.src = src;

script.onload = (e) => {
if (typeof initEvents.onload === "function") {
initEvents.onload(e as ScriptEvent);
} else if (typeof initEvents.onLoad === "function") {
initEvents.onLoad(e as ScriptEvent);
}

resolve(e);
};

script.onerror = (e) => {
if (typeof initEvents.onerror === "function") {
initEvents.onerror(e as ScriptEvent);
} else if (typeof initEvents.onError === "function") {
initEvents.onError(e as ScriptEvent);
}

reject(e);
};

// Apply additional attributes if any
Object.entries(otherAttributes).forEach(([key, value]) => {
script.setAttribute(key, value);
});

document.head.appendChild(script);
});

SCRIPT_PROMISES.set(src, promise);
return promise;
};

const createScript = (src: string, attributes: Readonly<ScriptAttributes> = {}) => {
const [script] = createResource(() => loadScript(src, attributes));
return script;
};

export default createScript;
3 changes: 1 addition & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
// Main library export site
// Use playground app (via Vite) to test and document the library
export { default as createScript } from "./createScript";
2 changes: 1 addition & 1 deletion tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default defineConfig((config) => {

const tsupOptions = preset
.generateTsupOptions(parsedOptions)
.map((tsupOption) => ({ name: "solid-js", ...tsupOption }));
.map((tsupOption) => ({ name: "solid-create-script", minify: true, ...tsupOption }));

return tsupOptions;
});

0 comments on commit c40ffa8

Please sign in to comment.