Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/script-loader-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@solid-primitives/script-loader": major
---

Migrate to Solid.js v2.0 (beta.10)

## Breaking Changes

**Peer dependencies**: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10` are now required.

### `@solid-primitives/script-loader`

- `isServer` and `spread` now imported from `@solidjs/web` (not `solid-js/web`)
- `ComponentProps` and `JSX` types now sourced from `@solidjs/web` for correct intrinsic element resolution
- `splitProps` (removed in Solid 2.0) replaced with plain object extraction
- Static script attributes applied via `assign` synchronously before reactive src tracking; this means attributes like `type` and `async` are set before the script is appended to the document, which is the correct order for browser loading
- `createRenderEffect` converted to the split compute/apply pattern required by Solid 2.0; src accessor is tracked in the compute phase and the DOM update applied in the apply phase
2 changes: 2 additions & 0 deletions packages/script-loader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ yarn add @solid-primitives/script-loader
pnpm add @solid-primitives/script-loader
```

Requires `solid-js` and `@solidjs/web` as peer dependencies.

## How to use it

createScriptLoader expects a props object with a `src` property. All the other props will be spread to the script element.
Expand Down
6 changes: 4 additions & 2 deletions packages/script-loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"test:ssr": "pnpm run vitest --mode ssr"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.10",
"solid-js": "^2.0.0-beta.10"
},
"keywords": [
"script",
Expand All @@ -54,6 +55,7 @@
],
"typesVersions": {},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "^2.0.0-beta.10",
"solid-js": "^2.0.0-beta.10"
}
}
84 changes: 41 additions & 43 deletions packages/script-loader/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import {
type Accessor,
createRenderEffect,
onCleanup,
splitProps,
type ComponentProps,
type JSX,
} from "solid-js";
import { spread, isServer } from "solid-js/web";
import { type Accessor, createRenderEffect, onCleanup } from "solid-js";
import { assign, isServer, type ComponentProps, type JSX } from "@solidjs/web";

export type ScriptProps = Omit<ComponentProps<"script">, "src" | "textContent"> & {
/** URL or source of the script to load. */
Expand All @@ -15,8 +8,6 @@ export type ScriptProps = Omit<ComponentProps<"script">, "src" | "textContent">
[dataAttribute: `data-${string}`]: any;
};

const OMITTED_PROPS = ["src"] as const;

/**
* Creates a convenient script loader utility
*
Expand All @@ -40,39 +31,46 @@ export function createScriptLoader(props: ScriptProps): HTMLScriptElement | unde
}
const script = document.createElement("script");
const eventKeys: string[] = Object.keys(props).filter(p => p.startsWith("on"));
const [local, events, scriptProps] = splitProps(
props,
OMITTED_PROPS,
eventKeys as readonly (keyof typeof props)[],
const { src: srcProp } = props;

const staticProps: Record<string, unknown> = {};
for (const [k, v] of Object.entries(props as Record<string, unknown>)) {
if (k !== "src" && !eventKeys.includes(k)) staticProps[k] = v;
}
assign(script, staticProps, true);

for (const name of eventKeys) {
const handler = props[name as keyof ScriptProps] as JSX.EventHandlerUnion<
HTMLScriptElement,
Event
>;
const eventName = /^on:?(.*)/.test(name)
? name.startsWith("on:")
? RegExp.$1
: RegExp.$1.toLowerCase()
: name;
script.addEventListener(eventName, (ev: Event) => {
Object.defineProperties(ev, {
target: { value: script, enumerable: true },
currentTarget: { value: script, enumerable: true },
});
Array.isArray(handler)
? handler[0](handler[1], ev)
: typeof handler === "function" && handler.call(null, Object.assign(ev));
});
}

createRenderEffect(
() => (typeof srcProp === "string" ? srcProp : srcProp()),
(src: string) => {
const prop = /^(https?:|\w[\.\w-_%]+|)\//.test(src) ? "src" : "textContent";
if (script[prop] !== src) {
script[prop] = src;
document.head.appendChild(script);
}
},
);
setTimeout(() => spread(script, scriptProps, false, true));
createRenderEffect(() => {
Object.entries(events).forEach(
([name, handler]: [string, JSX.EventHandlerUnion<HTMLScriptElement, Event>]) =>
script.addEventListener(
/^on:?(.*)/.test(name)
? name.startsWith("on:")
? RegExp.$1
: RegExp.$1.toLowerCase()
: name,
(ev: Event) => {
Object.defineProperties(ev, {
target: { value: script, enumerable: true },
currentTarget: { value: script, enumerable: true },
});
Array.isArray(handler)
? handler[0](handler[1], ev)
: typeof handler === "function" && handler.call(null, Object.assign(ev));
},
),
);
const src = typeof local.src === "string" ? local.src : local.src();
const prop = /^(https?:|\w[\.\w-_%]+|)\//.test(src) ? "src" : "textContent";
if (script[prop] !== src) {
script[prop] = src;
document.head.appendChild(script);
}
});

onCleanup(() => document.head.contains(script) && document.head.removeChild(script));
return script;
}
33 changes: 15 additions & 18 deletions packages/script-loader/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @vitest-environment node
import { createRoot, createSignal } from "solid-js";
import { createRoot, createSignal, flush } from "solid-js";
import { afterAll, describe, expect, it, vi } from "vitest";
import { createScriptLoader } from "../src/index.js";
import { JSDOM } from "jsdom";
Expand Down Expand Up @@ -97,23 +97,20 @@ describe("createScriptLoader", () => {

it("will update the url from an accessor", async () => {
const actualSrcUrls: (string | undefined)[] = [];
await new Promise<void>(resolve =>
createRoot(async dispose => {
const [src, setSrc] = createSignal("http://127.0.0.1:12345/script.js");
const script = createScriptLoader({
src: src,
onLoad: () => setSrc("http://127.0.0.1:12345/script2.js"),
});
vi.runAllTimers();
actualSrcUrls.push(script?.src);
await dispatchAndWait(script, "load");
queueMicrotask(() => {
actualSrcUrls.push(script?.src);
dispose();
resolve();
});
}),
);
const [src, setSrc] = createSignal("http://127.0.0.1:12345/script.js");
let dispose!: () => void;
const script = createRoot(d => {
dispose = d;
return createScriptLoader({
src: src,
onLoad: () => setSrc("http://127.0.0.1:12345/script2.js"),
});
});
actualSrcUrls.push(script?.src);
await dispatchAndWait(script, "load");
flush();
actualSrcUrls.push(script?.src);
dispose();
expect(actualSrcUrls).toEqual([
"http://127.0.0.1:12345/script.js",
"http://127.0.0.1:12345/script2.js",
Expand Down
34 changes: 32 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.