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 726871879..df67197eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -813,9 +813,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: @@ -2366,36 +2369,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-wasm@2.3.0': resolution: {integrity: sha512-ejBAX8H0ZGsD8lSICDNyMbSEtPMWgDL0WFCt/0z7hyf5v8Imz4rAM8xY379mBsECkq/Wdqa5WEDLqtjZ+6NxfA==} @@ -2506,36 +2515,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} @@ -2672,56 +2687,67 @@ packages: resolution: {integrity: sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.43.0': resolution: {integrity: sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.43.0': resolution: {integrity: sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.43.0': resolution: {integrity: sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.43.0': resolution: {integrity: sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.43.0': resolution: {integrity: sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.43.0': resolution: {integrity: sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.43.0': resolution: {integrity: sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.43.0': resolution: {integrity: sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.43.0': resolution: {integrity: sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.43.0': resolution: {integrity: sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.43.0': resolution: {integrity: sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==} @@ -5208,24 +5234,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}