diff --git a/.changeset/script-loader-solid2-migration.md b/.changeset/script-loader-solid2-migration.md new file mode 100644 index 000000000..88b42469b --- /dev/null +++ b/.changeset/script-loader-solid2-migration.md @@ -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 diff --git a/packages/script-loader/README.md b/packages/script-loader/README.md index de0a3f40b..3a568610d 100644 --- a/packages/script-loader/README.md +++ b/packages/script-loader/README.md @@ -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. diff --git a/packages/script-loader/package.json b/packages/script-loader/package.json index fd7085922..ede74fd47 100644 --- a/packages/script-loader/package.json +++ b/packages/script-loader/package.json @@ -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", @@ -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" } } diff --git a/packages/script-loader/src/index.ts b/packages/script-loader/src/index.ts index 9acb84b2c..35acf05fe 100644 --- a/packages/script-loader/src/index.ts +++ b/packages/script-loader/src/index.ts @@ -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, "src" | "textContent"> & { /** URL or source of the script to load. */ @@ -15,8 +8,6 @@ export type ScriptProps = Omit, "src" | "textContent"> [dataAttribute: `data-${string}`]: any; }; -const OMITTED_PROPS = ["src"] as const; - /** * Creates a convenient script loader utility * @@ -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 = {}; + for (const [k, v] of Object.entries(props as Record)) { + 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]) => - 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; } diff --git a/packages/script-loader/test/index.test.ts b/packages/script-loader/test/index.test.ts index f8a6a48bf..267218894 100644 --- a/packages/script-loader/test/index.test.ts +++ b/packages/script-loader/test/index.test.ts @@ -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"; @@ -97,23 +97,20 @@ describe("createScriptLoader", () => { it("will update the url from an accessor", async () => { const actualSrcUrls: (string | undefined)[] = []; - await new Promise(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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c536fac1..ac080626e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -822,9 +822,12 @@ importers: packages/script-loader: devDependencies: + '@solidjs/web': + specifier: ^2.0.0-beta.10 + version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: ^2.0.0-beta.10 + version: 2.0.0-beta.10 packages/scroll: dependencies: