From 81f059b79794a4b98f32ae67fc7401d4484f69c1 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Wed, 4 Mar 2026 18:43:36 -0600 Subject: [PATCH 1/8] solid v2 upgrade --- babel.config.cjs | 4 +- package.json | 10 ++- pnpm-lock.yaml | 173 +++++++++++++++++++++++++++++++------------- src/index.tsx | 18 +++-- test/index.spec.tsx | 20 ++++- 5 files changed, 158 insertions(+), 67 deletions(-) diff --git a/babel.config.cjs b/babel.config.cjs index f81ba93..477d6b2 100644 --- a/babel.config.cjs +++ b/babel.config.cjs @@ -9,10 +9,10 @@ module.exports = { [ "babel-plugin-jsx-dom-expressions", { - moduleName: "solid-js/web", + moduleName: "@solidjs/web", contextToCustomElements: true, wrapConditionals: true, - builtIns: ["For", "Show", "Switch", "Match", "Suspense", "SuspenseList", "Portal"] + builtIns: ["For", "Show", "Switch", "Match", "Loading", "Errored", "Portal"] } ] ] diff --git a/package.json b/package.json index e4da119..49074c2 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "release": "pnpm build && changeset publish" }, "peerDependencies": { - "solid-js": ">=1.8.4" + "solid-js": "2.0.0-beta.0", + "@solidjs/web": "2.0.0-beta.0" }, "devDependencies": { "@babel/cli": "^7.27.0", @@ -44,15 +45,16 @@ "@babel/preset-typescript": "7.27.0", "@changesets/cli": "^2.28.1", "@rollup/plugin-babel": "^6.0.4", + "@solidjs/web": "2.0.0-beta.0", "@testing-library/jest-dom": "^6.6.3", "@types/jest": "^29.5.14", "@vitest/browser": "^3.1.1", - "babel-preset-solid": "^1.9.5", + "babel-preset-solid": "2.0.0-beta.0", "jsdom": "^26.0.0", "rimraf": "^6.0.1", - "solid-js": "^1.9.5", + "solid-js": "2.0.0-beta.0", "typescript": "5.8.3", - "vite-plugin-solid": "^2.11.6", + "vite-plugin-solid": "^3.0.0-next.0", "vitest": "^3.1.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 295b8ce..12b59d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@rollup/plugin-babel': specifier: ^6.0.4 version: 6.0.4(@babel/core@7.26.10)(@types/babel__core@7.20.5)(rollup@4.39.0) + '@solidjs/web': + specifier: 2.0.0-beta.0 + version: 2.0.0-beta.0(solid-js@2.0.0-beta.0) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -36,8 +39,8 @@ importers: specifier: ^3.1.1 version: 3.1.1(vite@6.2.6(@types/node@22.14.1))(vitest@3.1.1) babel-preset-solid: - specifier: ^1.9.5 - version: 1.9.5(@babel/core@7.26.10) + specifier: 2.0.0-beta.0 + version: 2.0.0-beta.0(@babel/core@7.26.10)(solid-js@2.0.0-beta.0) jsdom: specifier: ^26.0.0 version: 26.0.0 @@ -45,14 +48,14 @@ importers: specifier: ^6.0.1 version: 6.0.1 solid-js: - specifier: ^1.9.5 - version: 1.9.5 + specifier: 2.0.0-beta.0 + version: 2.0.0-beta.0 typescript: specifier: 5.8.3 version: 5.8.3 vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.2.6(@types/node@22.14.1)) + specifier: ^3.0.0-next.0 + version: 3.0.0-next.1(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.0)(vite@6.2.6(@types/node@22.14.1)) vitest: specifier: ^3.1.1 version: 3.1.1(@types/node@22.14.1)(@vitest/browser@3.1.1)(jsdom@26.0.0) @@ -96,6 +99,10 @@ packages: resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.25.9': resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} engines: {node: '>=6.9.0'} @@ -167,14 +174,18 @@ packages: resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.22.5': - resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.25.9': resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.9': resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} @@ -196,6 +207,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} engines: {node: '>=6.9.0'} @@ -605,6 +621,10 @@ packages: resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@changesets/apply-release-plan@7.0.10': resolution: {integrity: sha512-wNyeIJ3yDsVspYvHnEz1xQDq18D9ifed3lI+wxRQRK4pArUcuHgCTrHv0QRnnwjhVCQACxZ+CBih3wgOct6UXw==} @@ -854,6 +874,9 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -872,6 +895,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1021,6 +1047,14 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@solidjs/signals@0.11.1': + resolution: {integrity: sha512-//nvM/Vjmp34utkMtf3l99+zuiT9gEF9709cGA5xHIsJgkKd4xhfU0getn8gujnypijddcS0VMTZrUhWsqoDfA==} + + '@solidjs/web@2.0.0-beta.0': + resolution: {integrity: sha512-eBmPNbzR6/GBrkNho2rQ89frd5Y/BKGqUQstM5mHBNs4s+pdHMn9Y7n6wcBbmfoSwJZRa4QxOl3K9Q7MkCZBNg==} + peerDependencies: + solid-js: ^2.0.0-beta.0 + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -1181,8 +1215,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - babel-plugin-jsx-dom-expressions@0.39.7: - resolution: {integrity: sha512-8GzVmFla7jaTNWW8W+lTMl9YGva4/06CtwJjySnkYtt8G1v9weCzc2SuF1DfrudcCNb2Doetc1FRg33swBYZCA==} + babel-plugin-jsx-dom-expressions@0.41.0-next.9: + resolution: {integrity: sha512-6EZcgFC8AM2lM2jSe7W/5fuREMC/PErTUD2O5YNuV267p8xaLFpoJPlM8PL0NY2Pu0a49gb3JbYvt5/yv2Z7Ww==} peerDependencies: '@babel/core': ^7.20.12 @@ -1201,10 +1235,14 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-preset-solid@1.9.5: - resolution: {integrity: sha512-85I3osODJ1LvZbv8wFozROV1vXq32BubqHXAGu73A//TRs3NLI1OFP83AQBUTSQHwgZQmARjHlJciym3we+V+w==} + babel-preset-solid@2.0.0-beta.0: + resolution: {integrity: sha512-bPA3YnISn4cUyMjIPlC3QxbX5tqCKYBLK6zhChhiJ6wKmKt6yv8pB4FmwyxGf3tcb0eARhcIYnXpxZ+z58Bmww==} peerDependencies: '@babel/core': ^7.0.0 + solid-js: ^2.0.0-beta.0 + peerDependenciesMeta: + solid-js: + optional: true balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2018,14 +2056,14 @@ packages: engines: {node: '>=10'} hasBin: true - seroval-plugins@1.2.1: - resolution: {integrity: sha512-H5vs53+39+x4Udwp4J5rNZfgFuA+Lt+uU+09w1gYBVWomtAl98B+E9w7yC05Xc81/HgLvJdlyqJbU0fJCKCmdw==} + seroval-plugins@1.5.0: + resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.2.1: - resolution: {integrity: sha512-yBxFFs3zmkvKNmR0pFSU//rIsYjuX418TnlDmc2weaq5XFDqDIV/NOMPBoLrbxjLH42p4UzRuXHryXh9dYcKcw==} + seroval@1.5.0: + resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} engines: {node: '>=10'} shebang-command@2.0.0: @@ -2055,13 +2093,13 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - solid-js@1.9.5: - resolution: {integrity: sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw==} + solid-js@2.0.0-beta.0: + resolution: {integrity: sha512-SnOAA8QBC+TJ3dwN9xPRDYybCZJ6BSW6gPKnDRtJ0n0r96pg/KcbvSt/T/O6qaWI1pf+CMpYmdOxZfTQr2AhQg==} - solid-refresh@0.6.3: - resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} + solid-refresh@0.8.0-next.2: + resolution: {integrity: sha512-fhJ3ZT8QOMvyvtF6KJqaI6vG8OK/EIcarNy9S0EsEmlin7qfh4XndSQWFMQyiyIA22rjbj0w5GXiaAwTLQSLXA==} peerDependencies: - solid-js: ^1.3 + solid-js: '>=2.0.0-beta.0 <2.0.0' source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -2213,12 +2251,12 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-plugin-solid@2.11.6: - resolution: {integrity: sha512-Sl5CTqJTGyEeOsmdH6BOgalIZlwH3t4/y0RQuFLMGnvWMBvxb4+lq7x3BSiAw6etf0QexfNJW7HSOO/Qf7pigg==} + vite-plugin-solid@3.0.0-next.1: + resolution: {integrity: sha512-Te38NOFOypn7dNpT1iCxNGtD1Fp2W5P885tS3wH36AOsJffvFfIsjhvx/FW5GVJzPuax8FajjM6AjgY1I+7wOQ==} peerDependencies: '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* - solid-js: ^1.7.2 - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + solid-js: '>=2.0.0-beta.0 <2.0.0' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: '@testing-library/jest-dom': optional: true @@ -2433,6 +2471,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.25.9': dependencies: '@babel/types': 7.27.0 @@ -2485,7 +2531,7 @@ snapshots: '@babel/helper-module-imports@7.18.6': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 '@babel/helper-module-imports@7.25.9': dependencies: @@ -2536,10 +2582,12 @@ snapshots: '@babel/helper-string-parser@7.25.9': {} - '@babel/helper-validator-identifier@7.22.5': {} + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.25.9': {} '@babel/helper-wrap-function@7.25.9': @@ -2557,7 +2605,7 @@ snapshots: '@babel/highlight@7.22.5': dependencies: - '@babel/helper-validator-identifier': 7.22.5 + '@babel/helper-validator-identifier': 7.25.9 chalk: 2.4.2 js-tokens: 4.0.0 @@ -2565,6 +2613,10 @@ snapshots: dependencies: '@babel/types': 7.27.0 + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -3084,6 +3136,11 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@changesets/apply-release-plan@7.0.10': dependencies: '@changesets/config': 3.1.1 @@ -3347,6 +3404,11 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -3364,6 +3426,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.27.0 @@ -3478,6 +3545,14 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@solidjs/signals@0.11.1': {} + + '@solidjs/web@2.0.0-beta.0(solid-js@2.0.0-beta.0)': + dependencies: + seroval: 1.5.0 + seroval-plugins: 1.5.0(seroval@1.5.0) + solid-js: 2.0.0-beta.0 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.22.5 @@ -3656,12 +3731,12 @@ snapshots: asynckit@0.4.0: {} - babel-plugin-jsx-dom-expressions@0.39.7(@babel/core@7.26.10): + babel-plugin-jsx-dom-expressions@0.41.0-next.9(@babel/core@7.26.10): dependencies: '@babel/core': 7.26.10 '@babel/helper-module-imports': 7.18.6 '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 html-entities: 2.3.3 parse5: 7.2.1 validate-html-nesting: 1.2.2 @@ -3690,10 +3765,12 @@ snapshots: transitivePeerDependencies: - supports-color - babel-preset-solid@1.9.5(@babel/core@7.26.10): + babel-preset-solid@2.0.0-beta.0(@babel/core@7.26.10)(solid-js@2.0.0-beta.0): dependencies: '@babel/core': 7.26.10 - babel-plugin-jsx-dom-expressions: 0.39.7(@babel/core@7.26.10) + babel-plugin-jsx-dom-expressions: 0.41.0-next.9(@babel/core@7.26.10) + optionalDependencies: + solid-js: 2.0.0-beta.0 balanced-match@1.0.2: {} @@ -4508,11 +4585,11 @@ snapshots: semver@7.7.1: {} - seroval-plugins@1.2.1(seroval@1.2.1): + seroval-plugins@1.5.0(seroval@1.5.0): dependencies: - seroval: 1.2.1 + seroval: 1.5.0 - seroval@1.2.1: {} + seroval@1.5.0: {} shebang-command@2.0.0: dependencies: @@ -4534,20 +4611,18 @@ snapshots: slash@3.0.0: {} - solid-js@1.9.5: + solid-js@2.0.0-beta.0: dependencies: + '@solidjs/signals': 0.11.1 csstype: 3.1.2 - seroval: 1.2.1 - seroval-plugins: 1.2.1(seroval@1.2.1) + seroval: 1.5.0 + seroval-plugins: 1.5.0(seroval@1.5.0) - solid-refresh@0.6.3(solid-js@1.9.5): + solid-refresh@0.8.0-next.2(solid-js@2.0.0-beta.0): dependencies: - '@babel/generator': 7.27.0 - '@babel/helper-module-imports': 7.25.9 - '@babel/types': 7.27.0 - solid-js: 1.9.5 - transitivePeerDependencies: - - supports-color + '@babel/generator': 7.29.1 + '@babel/types': 7.29.0 + solid-js: 2.0.0-beta.0 source-map-js@1.2.1: {} @@ -4686,14 +4761,14 @@ snapshots: - tsx - yaml - vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.2.6(@types/node@22.14.1)): + vite-plugin-solid@3.0.0-next.1(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.0)(vite@6.2.6(@types/node@22.14.1)): dependencies: '@babel/core': 7.26.10 '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.5(@babel/core@7.26.10) + babel-preset-solid: 2.0.0-beta.0(@babel/core@7.26.10)(solid-js@2.0.0-beta.0) merge-anything: 5.1.7 - solid-js: 1.9.5 - solid-refresh: 0.6.3(solid-js@1.9.5) + solid-js: 2.0.0-beta.0 + solid-refresh: 0.8.0-next.2(solid-js@2.0.0-beta.0) vite: 6.2.6(@types/node@22.14.1) vitefu: 1.0.6(vite@6.2.6(@types/node@22.14.1)) optionalDependencies: diff --git a/src/index.tsx b/src/index.tsx index a10da4e..c07654a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,12 +4,11 @@ import { createRenderEffect, createUniqueId, JSX, - onCleanup, ParentComponent, sharedConfig, useContext } from "solid-js"; -import { escape, isServer, spread, ssr, useAssets } from "solid-js/web"; +import { escape, isServer, spread, ssr, useAssets } from "@solidjs/web"; export const MetaContext = createContext(); @@ -58,7 +57,7 @@ const getTagKey = (tag: TagDescription, properties: string[]) => { }; function initClientProvider() { - if (!sharedConfig.context) { + if (!sharedConfig.hydrating) { const ssrTags = document.head.querySelectorAll(`[data-sm]`); // `forEach` on `NodeList` is not supported in Googlebot, so use a workaround Array.prototype.forEach.call(ssrTags, (ssrTag: Node) => ssrTag.parentNode!.removeChild(ssrTag)); @@ -200,7 +199,7 @@ export const MetaProvider: ParentComponent = props => { const actions = !isServer ? initClientProvider() : initServerProvider(); - return {props.children}; + return {props.children}; }; const MetaTag = ( @@ -225,10 +224,13 @@ export function useHead(tagDesc: TagDescription) { const c = useContext(MetaContext); if (!c) throw new Error(" should be in the tree"); - createRenderEffect(() => { - const index = c!.addTag(tagDesc); - onCleanup(() => c!.removeTag(tagDesc, index)); - }); + createRenderEffect( + () => tagDesc, + tag => { + const index = c!.addTag(tag); + return () => c!.removeTag(tag, index); + } + ); } function renderTags(tags: Array) { diff --git a/test/index.spec.tsx b/test/index.spec.tsx index 6487447..13fc7c0 100644 --- a/test/index.spec.tsx +++ b/test/index.spec.tsx @@ -1,6 +1,6 @@ /* @jsxImportSource solid-js */ -import { createSignal, getOwner, lazy } from "solid-js"; -import { hydrate, render, Show } from "solid-js/web"; +import { createSignal, flush, getOwner, lazy } from "solid-js"; +import { hydrate, render, Show } from "@solidjs/web"; import { MetaProvider, Title, Style, Meta, Link, Base } from "../src"; import { hydrationScript, removeScript } from "./hydration_script"; import { describe, test, expect, beforeEach, afterEach } from "vitest"; @@ -85,6 +85,7 @@ test("unmount middle child, should show only the last title", () => { ); expect(document.head.innerHTML).toBe(snapshot); setVisible(false); + flush(); expect(document.head.innerHTML).toBe(snapshot); dispose(); }); @@ -114,6 +115,7 @@ test("unmount last child, should show only the second last title", () => { ); expect(document.head.innerHTML).toBe(snapshot1); setVisible(false); + flush(); expect(document.head.innerHTML).toBe(snapshot2); dispose(); }); @@ -163,8 +165,10 @@ test("mounts and unmounts title", () => { expect(document.head.innerHTML).toBe(snapshot1); setVisible(true); + flush(); expect(document.head.innerHTML).toBe(snapshot2); setVisible(false); + flush(); expect(document.head.innerHTML).toBe(snapshot1); dispose(); }); @@ -190,8 +194,10 @@ test("hydrates and unmounts title", () => { expect(document.head.innerHTML).toBe(snapshot1); setVisible(true); + flush(); expect(document.head.innerHTML).toBe(snapshot2); setVisible(false); + flush(); expect(document.head.innerHTML).toBe(snapshot1); dispose(); removeScript(); @@ -230,6 +236,7 @@ test("switches between titles", async () => { await new Promise(resolve => setTimeout(resolve, 1)); expect(document.head.innerHTML).toBe(snapshot1); setVisible(false); + flush(); await new Promise(resolve => setTimeout(resolve, 1)); expect(document.head.innerHTML).toBe(snapshot2); dispose(); @@ -268,14 +275,18 @@ test("renders only the last meta with the same name", () => { expect(document.head.innerHTML).toBe(snapshot1); // mount first setVisible1(true); + flush(); expect(document.head.innerHTML).toBe(snapshot2); // mount second setVisible2(true); + flush(); expect(document.head.innerHTML).toBe(snapshot3); // unmount second setVisible2(false); + flush(); // unmount first setVisible1(false); + flush(); expect(document.head.innerHTML).toBe(snapshot1); dispose(); }); @@ -322,7 +333,7 @@ test("throws error if head tag is rendered without MetaProvider", () => { expect(() => { let div = document.createElement("div"); render(() => , div); - }).toThrowError(/ should be in the tree/); + }).toThrowError(/Context must either be created with a default value/); }); test("doesn't create any effect on removal", () => { @@ -330,7 +341,7 @@ test("doesn't create any effect on removal", () => { const [show, setShow] = createSignal(true); const showAndTest = () => { - expect(getOwner()?.owner).toBeTruthy(); + expect(getOwner()?._parent).toBeTruthy(); return show(); }; @@ -348,6 +359,7 @@ test("doesn't create any effect on removal", () => { ); setShow(false); + flush(); dispose(); }); From ea677ddc61dae4b60be3b2e06e4101fc8b944458 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Tue, 17 Mar 2026 12:40:10 -0700 Subject: [PATCH 2/8] update Solid 2 beta compatibility Broaden the Solid 2 peer ranges and align the local beta toolchain so solid-meta tests against the current runtime contract. Made-with: Cursor --- package.json | 12 ++-- pnpm-lock.yaml | 154 ++++++++++++++++++-------------------------- test/index.spec.tsx | 10 +-- 3 files changed, 76 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index 49074c2..8345518 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "release": "pnpm build && changeset publish" }, "peerDependencies": { - "solid-js": "2.0.0-beta.0", - "@solidjs/web": "2.0.0-beta.0" + "solid-js": ">=2.0.0-beta.0 <2.0.0", + "@solidjs/web": ">=2.0.0-beta.0 <2.0.0" }, "devDependencies": { "@babel/cli": "^7.27.0", @@ -45,16 +45,16 @@ "@babel/preset-typescript": "7.27.0", "@changesets/cli": "^2.28.1", "@rollup/plugin-babel": "^6.0.4", - "@solidjs/web": "2.0.0-beta.0", + "@solidjs/web": "2.0.0-beta.3", "@testing-library/jest-dom": "^6.6.3", "@types/jest": "^29.5.14", "@vitest/browser": "^3.1.1", - "babel-preset-solid": "2.0.0-beta.0", + "babel-preset-solid": "2.0.0-beta.3", "jsdom": "^26.0.0", "rimraf": "^6.0.1", - "solid-js": "2.0.0-beta.0", + "solid-js": "2.0.0-beta.3", "typescript": "5.8.3", - "vite-plugin-solid": "^3.0.0-next.0", + "vite-plugin-solid": "^3.0.0-next.2", "vitest": "^3.1.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12b59d5..b4bd147 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^6.0.4 version: 6.0.4(@babel/core@7.26.10)(@types/babel__core@7.20.5)(rollup@4.39.0) '@solidjs/web': - specifier: 2.0.0-beta.0 - version: 2.0.0-beta.0(solid-js@2.0.0-beta.0) + specifier: 2.0.0-beta.3 + version: 2.0.0-beta.3(@solidjs/signals@0.13.3)(solid-js@2.0.0-beta.3) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -39,8 +39,8 @@ importers: specifier: ^3.1.1 version: 3.1.1(vite@6.2.6(@types/node@22.14.1))(vitest@3.1.1) babel-preset-solid: - specifier: 2.0.0-beta.0 - version: 2.0.0-beta.0(@babel/core@7.26.10)(solid-js@2.0.0-beta.0) + specifier: 2.0.0-beta.3 + version: 2.0.0-beta.3(@babel/core@7.26.10)(solid-js@2.0.0-beta.3) jsdom: specifier: ^26.0.0 version: 26.0.0 @@ -48,14 +48,14 @@ importers: specifier: ^6.0.1 version: 6.0.1 solid-js: - specifier: 2.0.0-beta.0 - version: 2.0.0-beta.0 + specifier: 2.0.0-beta.3 + version: 2.0.0-beta.3 typescript: specifier: 5.8.3 version: 5.8.3 vite-plugin-solid: - specifier: ^3.0.0-next.0 - version: 3.0.0-next.1(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.0)(vite@6.2.6(@types/node@22.14.1)) + specifier: ^3.0.0-next.2 + version: 3.0.0-next.2(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.3)(vite@6.2.6(@types/node@22.14.1)) vitest: specifier: ^3.1.1 version: 3.1.1(@types/node@22.14.1)(@vitest/browser@3.1.1)(jsdom@26.0.0) @@ -170,18 +170,10 @@ packages: resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} @@ -877,18 +869,10 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} @@ -1047,13 +1031,14 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@solidjs/signals@0.11.1': - resolution: {integrity: sha512-//nvM/Vjmp34utkMtf3l99+zuiT9gEF9709cGA5xHIsJgkKd4xhfU0getn8gujnypijddcS0VMTZrUhWsqoDfA==} + '@solidjs/signals@0.13.3': + resolution: {integrity: sha512-Rlq8Kc0WtuhPv8SDNT1ANh45OUIbFKnY7AOiNmCSeOaGKlFaaYPPDxXN8qJtojykwGGwWXV6ZmHwCb1ZeY0b/A==} - '@solidjs/web@2.0.0-beta.0': - resolution: {integrity: sha512-eBmPNbzR6/GBrkNho2rQ89frd5Y/BKGqUQstM5mHBNs4s+pdHMn9Y7n6wcBbmfoSwJZRa4QxOl3K9Q7MkCZBNg==} + '@solidjs/web@2.0.0-beta.3': + resolution: {integrity: sha512-DpTIbba7pLTKIXxUblok4n1rQscBLbjhJKhk+eUoODOnAS4TVDV4OmZ6qhC+VEuyPAbIbqzfoZsectFNEhak3w==} peerDependencies: - solid-js: ^2.0.0-beta.0 + '@solidjs/signals': ^0.13.3 + solid-js: ^2.0.0-beta.3 '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -1235,11 +1220,11 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-preset-solid@2.0.0-beta.0: - resolution: {integrity: sha512-bPA3YnISn4cUyMjIPlC3QxbX5tqCKYBLK6zhChhiJ6wKmKt6yv8pB4FmwyxGf3tcb0eARhcIYnXpxZ+z58Bmww==} + babel-preset-solid@2.0.0-beta.3: + resolution: {integrity: sha512-hV8Gi0Akolju1ydXNZNmt/SWcvGSWX5mqg+HHQFU2/iz22MDMflRSJrhaqrmN0gxJin9s3A/x3A20HqmcGhkBg==} peerDependencies: '@babel/core': ^7.0.0 - solid-js: ^2.0.0-beta.0 + solid-js: ^2.0.0-beta.3 peerDependenciesMeta: solid-js: optional: true @@ -2093,8 +2078,8 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - solid-js@2.0.0-beta.0: - resolution: {integrity: sha512-SnOAA8QBC+TJ3dwN9xPRDYybCZJ6BSW6gPKnDRtJ0n0r96pg/KcbvSt/T/O6qaWI1pf+CMpYmdOxZfTQr2AhQg==} + solid-js@2.0.0-beta.3: + resolution: {integrity: sha512-s2UT66v1UpmM1QybhNrQlJUiSG1HYl2TM7ah2d0TETy5U4fLxIYbyVECB3YOw7GwOTzaGjjJjlg4A7hkYqCQ+g==} solid-refresh@0.8.0-next.2: resolution: {integrity: sha512-fhJ3ZT8QOMvyvtF6KJqaI6vG8OK/EIcarNy9S0EsEmlin7qfh4XndSQWFMQyiyIA22rjbj0w5GXiaAwTLQSLXA==} @@ -2251,8 +2236,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-plugin-solid@3.0.0-next.1: - resolution: {integrity: sha512-Te38NOFOypn7dNpT1iCxNGtD1Fp2W5P885tS3wH36AOsJffvFfIsjhvx/FW5GVJzPuax8FajjM6AjgY1I+7wOQ==} + vite-plugin-solid@3.0.0-next.2: + resolution: {integrity: sha512-13AjTjjvrit4QfLtygAEC2pnYWgawowniqBHBtjNnpltnjIzYd8YhPbBv1NEBf3jcFQtQZEQib1tFiIRAxba6w==} peerDependencies: '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* solid-js: '>=2.0.0-beta.0 <2.0.0' @@ -2406,8 +2391,8 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@asamuzakjp/css-color@3.1.1': dependencies: @@ -2437,7 +2422,7 @@ snapshots: '@babel/code-frame@7.26.2': dependencies: - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -2465,10 +2450,10 @@ snapshots: '@babel/generator@7.27.0': dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/generator@7.29.1': @@ -2525,7 +2510,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -2544,14 +2529,14 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.27.0 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 '@babel/helper-plugin-utils@7.26.5': {} @@ -2580,12 +2565,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.25.9': {} - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.25.9': {} @@ -2594,24 +2575,24 @@ snapshots: dependencies: '@babel/template': 7.27.0 '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helpers@7.27.0': dependencies: '@babel/template': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 '@babel/highlight@7.22.5': dependencies: - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.28.5 chalk: 2.4.2 js-tokens: 4.0.0 '@babel/parser@7.27.0': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 '@babel/parser@7.29.0': dependencies: @@ -2844,7 +2825,7 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.27.0 transitivePeerDependencies: - supports-color @@ -3091,7 +3072,7 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 esutils: 2.0.3 '@babel/preset-typescript@7.27.0(@babel/core@7.26.10)': @@ -3116,16 +3097,16 @@ snapshots: '@babel/template@7.27.0': dependencies: '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@babel/traverse@7.27.0': dependencies: '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/parser': 7.27.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 '@babel/template': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: @@ -3133,8 +3114,8 @@ snapshots: '@babel/types@7.27.0': dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/types@7.29.0': dependencies: @@ -3409,16 +3390,8 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/gen-mapping@0.3.8': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': @@ -3545,13 +3518,14 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@solidjs/signals@0.11.1': {} + '@solidjs/signals@0.13.3': {} - '@solidjs/web@2.0.0-beta.0(solid-js@2.0.0-beta.0)': + '@solidjs/web@2.0.0-beta.3(@solidjs/signals@0.13.3)(solid-js@2.0.0-beta.3)': dependencies: + '@solidjs/signals': 0.13.3 seroval: 1.5.0 seroval-plugins: 1.5.0(seroval@1.5.0) - solid-js: 2.0.0-beta.0 + solid-js: 2.0.0-beta.3 '@testing-library/dom@10.4.0': dependencies: @@ -3582,24 +3556,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.20.1 '@types/babel__generator@7.6.4': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 '@types/babel__template@7.4.1': dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__traverse@7.20.1': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.29.0 '@types/estree@1.0.7': {} @@ -3765,12 +3739,12 @@ snapshots: transitivePeerDependencies: - supports-color - babel-preset-solid@2.0.0-beta.0(@babel/core@7.26.10)(solid-js@2.0.0-beta.0): + babel-preset-solid@2.0.0-beta.3(@babel/core@7.26.10)(solid-js@2.0.0-beta.3): dependencies: '@babel/core': 7.26.10 babel-plugin-jsx-dom-expressions: 0.41.0-next.9(@babel/core@7.26.10) optionalDependencies: - solid-js: 2.0.0-beta.0 + solid-js: 2.0.0-beta.3 balanced-match@1.0.2: {} @@ -4611,18 +4585,18 @@ snapshots: slash@3.0.0: {} - solid-js@2.0.0-beta.0: + solid-js@2.0.0-beta.3: dependencies: - '@solidjs/signals': 0.11.1 + '@solidjs/signals': 0.13.3 csstype: 3.1.2 seroval: 1.5.0 seroval-plugins: 1.5.0(seroval@1.5.0) - solid-refresh@0.8.0-next.2(solid-js@2.0.0-beta.0): + solid-refresh@0.8.0-next.2(solid-js@2.0.0-beta.3): dependencies: '@babel/generator': 7.29.1 '@babel/types': 7.29.0 - solid-js: 2.0.0-beta.0 + solid-js: 2.0.0-beta.3 source-map-js@1.2.1: {} @@ -4761,14 +4735,14 @@ snapshots: - tsx - yaml - vite-plugin-solid@3.0.0-next.1(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.0)(vite@6.2.6(@types/node@22.14.1)): + vite-plugin-solid@3.0.0-next.2(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.3)(vite@6.2.6(@types/node@22.14.1)): dependencies: '@babel/core': 7.26.10 '@types/babel__core': 7.20.5 - babel-preset-solid: 2.0.0-beta.0(@babel/core@7.26.10)(solid-js@2.0.0-beta.0) + babel-preset-solid: 2.0.0-beta.3(@babel/core@7.26.10)(solid-js@2.0.0-beta.3) merge-anything: 5.1.7 - solid-js: 2.0.0-beta.0 - solid-refresh: 0.8.0-next.2(solid-js@2.0.0-beta.0) + solid-js: 2.0.0-beta.3 + solid-refresh: 0.8.0-next.2(solid-js@2.0.0-beta.3) vite: 6.2.6(@types/node@22.14.1) vitefu: 1.0.6(vite@6.2.6(@types/node@22.14.1)) optionalDependencies: diff --git a/test/index.spec.tsx b/test/index.spec.tsx index 13fc7c0..7c56195 100644 --- a/test/index.spec.tsx +++ b/test/index.spec.tsx @@ -1,6 +1,6 @@ /* @jsxImportSource solid-js */ import { createSignal, flush, getOwner, lazy } from "solid-js"; -import { hydrate, render, Show } from "@solidjs/web"; +import { hydrate, render, Show, Loading } from "@solidjs/web"; import { MetaProvider, Title, Style, Meta, Link, Base } from "../src"; import { hydrationScript, removeScript } from "./hydration_script"; import { describe, test, expect, beforeEach, afterEach } from "vitest"; @@ -225,9 +225,11 @@ test("switches between titles", async () => { () => ( Static - }> - - + + }> + + + ), div From 3c4d5ec6e2382a108847f4ef8dcd10c00fb0fdd4 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Tue, 17 Mar 2026 12:40:36 -0700 Subject: [PATCH 3/8] v0.30.0-next.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8345518..176e5a8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@solidjs/meta", "description": "Write meta tags to the document head", - "version": "0.29.7", + "version": "0.30.0-next.0", "author": "Ryan Carniato", "license": "MIT", "repository": { From 37caba2363cbe5dc92d565d041c1af2337fd076e Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Wed, 5 Aug 2026 01:30:36 -0700 Subject: [PATCH 4/8] Solid Meta 1.0: thin layer over Solid 2.0's useHead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilt provider-less on the core head registry in @solidjs/web 2.0.0-beta.31: MetaProvider/MetaContext and the legacy useHead shim are gone, components register HeadTag descriptors directly, and a new groups children into one replaceable set (with World'; + const dispose = render(() => <Title>{text}, div); + await tick(); + expect(document.title).toBe(text); + expect(document.head.querySelectorAll("script").length).toBe(0); + + dispose(); + await tick(); + }); + + test("Script renders an inline JSON-LD body", async () => { + const div = document.createElement("div"); + const jsonLd = JSON.stringify({ "@type": "WebSite", name: "Solid" }); + const dispose = render( + () => ( + + ), + div + ); + await tick(); + const script = document.head.querySelector('script[type="application/ld+json"]')!; + expect(JSON.parse(script.textContent!)).toEqual({ "@type": "WebSite", name: "Solid" }); + + dispose(); + await tick(); + expect(document.head.querySelector("script")).toBe(null); + }); + + test("stylesheets are removed on unmount; fetch hints persist", async () => { + vi.useFakeTimers(); + const div = document.createElement("div"); + const dispose = render( + () => ( + <> + + + + ), + div + ); + const sheet = document.head.querySelector('link[rel="stylesheet"]')!; + expect(sheet.getAttribute("href")).toBe("/page.css"); + expect(document.head.querySelectorAll('link[rel="preload"]').length).toBe(1); + + dispose(); + await tick(); + vi.runAllTimers(); + expect(sheet.isConnected).toBe(false); + // Retracting a hint is pointless churn: it stays for the session. + expect(document.head.querySelectorAll('link[rel="preload"]').length).toBe(1); + document.head.querySelector('link[rel="preload"]')!.remove(); + vi.useRealTimers(); + }); + + test("useHead re-export registers an array as one group", async () => { + const div = document.createElement("div"); + const dispose = render(() => { + useHead([ + { tag: "meta", props: { property: "og:image", content: "/a.png" } }, + { tag: "meta", props: { property: "og:image", content: "/b.png" } } + ]); + return null; + }, div); + await tick(); + const imgs = [...document.head.querySelectorAll('meta[property="og:image"]')]; + expect(imgs.map(m => m.getAttribute("content"))).toEqual(["/a.png", "/b.png"]); -test("doesn't create any effect on removal", () => { - let div = document.createElement("div"); - - const [show, setShow] = createSignal(true); - const showAndTest = () => { - expect(getOwner()?._parent).toBeTruthy(); - return show(); - }; - - const dispose = render( - () => ( - - - - Something {showAndTest()} that forces the Solid compiler to create a memo here - - - - ), - div - ); - - setShow(false); - flush(); - dispose(); + dispose(); + await tick(); + expect(document.head.querySelectorAll('meta[property="og:image"]').length).toBe(0); + }); }); -test("Escaping the title tag", () => { - let div = document.createElement("div"); - const snapshot = - 'Hello</title><script>alert("inject");</script><title> World'; - const dispose = render( - () => ( - -
- {'Hello World'} -
-
- ), - div - ); - expect(document.head.innerHTML).toBe(snapshot); - dispose(); +describe(" groups", () => { + test("groups same-identity tags and replaces the set atomically", async () => { + const div = document.createElement("div"); + const [page, setPage] = createSignal(false); + const dispose = render( + () => ( + <> + + + + + + + + + + + ), + div + ); + await tick(); + let imgs = [...document.head.querySelectorAll('meta[property="og:image"]')]; + expect(imgs.map(m => m.getAttribute("content"))).toEqual(["/a.png", "/b.png"]); + + setPage(true); + flush(); + await tick(); + imgs = [...document.head.querySelectorAll('meta[property="og:image"]')]; + expect(imgs.map(m => m.getAttribute("content"))).toEqual(["/c.png"]); + + setPage(false); + flush(); + await tick(); + imgs = [...document.head.querySelectorAll('meta[property="og:image"]')]; + expect(imgs.map(m => m.getAttribute("content"))).toEqual(["/a.png", "/b.png"]); + + dispose(); + await tick(); + expect(document.head.querySelectorAll('meta[property="og:image"]').length).toBe(0); + }); + + test("group membership is reactive, including through component calls", async () => { + const OgImage = (props: { src: string }) => ; + const div = document.createElement("div"); + const [extra, setExtra] = createSignal(false); + const dispose = render( + () => ( + + + + + + + ), + div + ); + await tick(); + let imgs = [...document.head.querySelectorAll('meta[property="og:image"]')]; + expect(imgs.map(m => m.getAttribute("content"))).toEqual(["/a.png"]); + + setExtra(true); + flush(); + await tick(); + imgs = [...document.head.querySelectorAll('meta[property="og:image"]')]; + expect(imgs.map(m => m.getAttribute("content"))).toEqual(["/a.png", "/b.png"]); + + setExtra(false); + flush(); + await tick(); + imgs = [...document.head.querySelectorAll('meta[property="og:image"]')]; + expect(imgs.map(m => m.getAttribute("content"))).toEqual(["/a.png"]); + + dispose(); + await tick(); + }); + + test("a nested forms its own independent group", async () => { + const div = document.createElement("div"); + const [inner, setInner] = createSignal(true); + const dispose = render( + () => ( + + + + + + + + + ), + div + ); + await tick(); + // The inner group commits later, so it wins the identity outright — + // it did not merge into the outer set. + let imgs = [...document.head.querySelectorAll('meta[property="og:image"]')]; + expect(imgs.map(m => m.getAttribute("content"))).toEqual(["/inner.png"]); + + setInner(false); + flush(); + await tick(); + imgs = [...document.head.querySelectorAll('meta[property="og:image"]')]; + expect(imgs.map(m => m.getAttribute("content"))).toEqual(["/outer.png"]); + + dispose(); + await tick(); + }); }); -test("Escaping the title meta", () => { - let div = document.createElement("div"); - const snapshot = ''; +describe("hydration", () => { + test("claims server-rendered head tags instead of recreating them", async () => { + hydrationScript(); + document.head.innerHTML = + 'Hydrated' + + ''; + const serverTitle = document.head.querySelector("title"); + const serverMeta = document.head.querySelector("meta"); + const div = document.createElement("div"); + const dispose = hydrate( + () => ( + <> + Hydrated + + + ), + div + ); + // During hydration the registry defers and retries on macrotasks. + await new Promise(resolve => setTimeout(resolve, 10)); + expect(document.querySelectorAll("title").length).toBe(1); + expect(document.head.querySelector("title")).toBe(serverTitle); + expect(document.head.querySelectorAll('meta[name="description"]').length).toBe(1); + expect(document.head.querySelector('meta[name="description"]')).toBe(serverMeta); - const dispose = render( - () => ( - -
- -
-
- ), - div - ); - expect(document.head.innerHTML).toBe(snapshot); - dispose(); + dispose(); + removeScript(); + await new Promise(resolve => setTimeout(resolve, 10)); + expect(document.head.querySelector('meta[name="description"]')).toBe(null); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 3c184fd..a82ae45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,9 +4,7 @@ "declaration": true, "target": "esnext", "moduleResolution": "bundler", - "strict": true, - "jsx": "preserve", - "jsxImportSource": "solid-js" + "strict": true }, "include": [ "./src" diff --git a/tsconfig.test.json b/tsconfig.test.json index 36d0dfb..c766415 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -6,7 +6,7 @@ "strict": true, "lib": ["dom", "esnext", "dom.iterable"], "jsx": "preserve", - "jsxImportSource": "solid-js" + "jsxImportSource": "@solidjs/web" }, "include": [ "./test" diff --git a/vitest.config.ts b/vitest.config.ts index ba268b5..9072d41 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,19 +1,11 @@ import { defineConfig } from 'vitest/config'; import solidPlugin from 'vite-plugin-solid'; -import { babel } from '@rollup/plugin-babel'; export default defineConfig({ plugins: [ - solidPlugin(), - babel({ - babelHelpers: 'bundled', - presets: [ - '@babel/preset-env', - '@babel/preset-typescript', - ['babel-preset-solid', { generate: 'dom', hydratable: true }] - ], - extensions: ['.js', '.jsx', '.ts', '.tsx'] - }) + // hot: false — solid-refresh's HMR wrapper trips STRICT_READ_UNTRACKED + // warnings under vitest and HMR is meaningless in tests. + solidPlugin({ hot: false }) ], test: { environment: 'jsdom', From 5a8b1aa63f73bc14bd5943b4748db0f9d920e392 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Wed, 5 Aug 2026 01:34:42 -0700 Subject: [PATCH 5/8] v1.0.0-next.0 --- .changeset/pre.json | 10 ++++++++++ CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000..dc32b87 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,10 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@solidjs/meta": "0.30.0-next.0" + }, + "changesets": [ + "solid-meta-1-0" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index b967ef9..6bfac7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # @solidjs/meta +## 1.0.0-next.0 + +### Major Changes + +- 37caba2: Solid Meta 1.0: rebuilt as a thin layer over Solid 2.0's `useHead` primitive. + + - **Provider-less**: the head registry is ambient. `MetaProvider` and + `MetaContext` are gone — remove the wrapper and the `tags={[]}` SSR + plumbing; on the server, head tags are spliced into `` automatically + by `renderToString`/`renderToStream` (or collected via the `onHead` render + option). + - **New components**: `` groups its child tags into one replacement + set (several `og:image`s that override as a unit and restore on disposal); + `", + "rest": "" +} \ No newline at end of file diff --git a/test/harness/__artifacts__/title-async.json b/test/harness/__artifacts__/title-async.json new file mode 100644 index 0000000..e136ceb --- /dev/null +++ b/test/harness/__artifacts__/title-async.json @@ -0,0 +1,5 @@ +{ + "name": "title-async", + "shell": "Page Async

Hello world

", + "rest": "" +} \ No newline at end of file diff --git a/test/harness/__artifacts__/title-dynamic-sync.json b/test/harness/__artifacts__/title-dynamic-sync.json new file mode 100644 index 0000000..e7db434 --- /dev/null +++ b/test/harness/__artifacts__/title-dynamic-sync.json @@ -0,0 +1,5 @@ +{ + "name": "title-dynamic-sync", + "shell": "My Solid Site

Framework: Solid

", + "rest": "" +} \ No newline at end of file diff --git a/test/harness/scenarios.tsx b/test/harness/scenarios.tsx new file mode 100644 index 0000000..511aa4c --- /dev/null +++ b/test/harness/scenarios.tsx @@ -0,0 +1,125 @@ +/** @jsxImportSource @solidjs/web */ + +// Shared scenario apps for the SSR -> hydrate parity harness. The server +// project (vitest.config.ssr.ts) renders these with the ssr generate and +// writes chunk artifacts; the hydrate project (vitest.config.hydrate.ts) +// replays those artifacts into jsdom and hydrates the same source compiled +// with the dom generate. Any asymmetry in owner/hydration-id allocation +// between the two runtimes shows up as unclaimed nodes, re-run async +// factories, or hydration warnings. +import { createMemo, createSignal, Loading } from "solid-js"; +import { Meta, Title } from "../../src"; + +export interface Scenario { + name: string; + App: () => any; + /** document.title once hydration settles */ + expectedTitle: string; + /** body textContent once hydration settles */ + expectedText: string; + /** whether the scenario streams async content (settle waits + streamed mode) */ + async?: boolean; + /** fire a client-side update after hydration */ + update?: () => void; + expectedTitleAfterUpdate?: string; + expectedTextAfterUpdate?: string; +} + +function asyncValue(value: T, ms: number): Promise { + return new Promise(r => setTimeout(() => r(value), ms)); +} + +// --- title-dynamic-sync ----------------------------------------------------- +// Multi-part Title children (`My {name()} Site`) compile to an array with a +// per-hole computation allocated inside the descriptor's children getter. +// Without registration-time pinning that allocation happens in the registry +// effect on the client but at stream flush on the server, drifting every +// hydration id allocated after it. +const [name, setName] = createSignal("Solid"); + +const titleDynamicSync: Scenario = { + name: "title-dynamic-sync", + App: () => ( + + + + + +
+ My {name()} Site +

Framework: {name()}

+
+ + + ), + expectedTitle: "My Solid Site", + expectedText: "Framework: Solid", + update: () => setName("Fine"), + expectedTitleAfterUpdate: "My Fine Site", + expectedTextAfterUpdate: "Framework: Fine" +}; + +// --- title-async ------------------------------------------------------------ +// An async read inside the Title body plus serialized async content after it, +// at the root (no boundary): the pending head prop holds the streaming shell. +const titleAsync: Scenario = { + name: "title-async", + App: () => { + const site = createMemo(() => asyncValue("Async", 5)); + const data = createMemo(() => asyncValue("world", 10)); + return ( + + + +
+ Page {site()} +

Hello {data()}

+
+ + + ); + }, + expectedTitle: "Page Async", + expectedText: "Hello world", + async: true +}; + +// --- title-async-boundary --------------------------------------------------- +// The reported shape: an async Title body inside a Loading boundary with +// sibling content and templates after it. Element hydration keys (_hk) share +// the id counter with computations, so if the title body's inner computation +// allocates at fragment flush on the server but in the registry effect on the +// client, every template claimed after it drifts and hydration mismatches. +const [suffix, setSuffix] = createSignal(""); + +const titleAsyncBoundary: Scenario = { + name: "title-async-boundary", + App: () => { + const site = createMemo(() => asyncValue("Async", 5)); + const data = createMemo(() => asyncValue("world", 10)); + return ( + + + +
+ Loading...}> + Page {site()} +
+

Hello {data()}

+

Static tail{suffix()}

+
+
+
+ + + ); + }, + expectedTitle: "Page Async", + expectedText: "Hello worldStatic tail", + async: true, + update: () => setSuffix("!"), + expectedTitleAfterUpdate: "Page Async", + expectedTextAfterUpdate: "Hello worldStatic tail!" +}; + +export const scenarios: Scenario[] = [titleDynamicSync, titleAsync, titleAsyncBoundary]; diff --git a/test/hydration/head-parity.spec.tsx b/test/hydration/head-parity.spec.tsx new file mode 100644 index 0000000..53b7409 --- /dev/null +++ b/test/hydration/head-parity.spec.tsx @@ -0,0 +1,136 @@ +/** @jsxImportSource @solidjs/web */ + +// Client half of the head hydration parity harness. Replays the streamed +// chunks written by test/server/head-harness.spec.tsx into jsdom's document +// and hydrates the identically-sourced app (dom generate). Invariants: +// +// 1. No console.warn — unclaimed-node reports and claim mismatches from the +// dev runtime fail the test. +// 2. Server-rendered body elements keep identity through hydration — an +// owner/hydration-id drift (e.g. a head-tag body allocating its inner +// computation at a different point on each runtime) makes claims miss +// and nodes get recreated. Claim failures also surface as thrown +// "Hydration Mismatch" errors, failing the test outright. +// 3. document.title and body text are correct after settle, and client-side +// updates still work. +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { flush } from "solid-js"; +import { hydrate } from "@solidjs/web"; +import { scenarios, type Scenario } from "../harness/scenarios.jsx"; + +const artifactsDir = resolve(dirname(fileURLToPath(import.meta.url)), "../harness/__artifacts__"); + +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + +function loadArtifact(name: string): { shell: string; rest: string } { + const file = resolve(artifactsDir, `${name}.json`); + if (!existsSync(file)) { + throw new Error( + `Missing artifact for scenario "${name}". Run the server harness first: ` + + `vitest run -c vitest.config.ssr.ts` + ); + } + return JSON.parse(readFileSync(file, "utf-8")); +} + +const scriptRe = /]*)>([\s\S]*?)<\/script>/g; + +function applyShell(shell: string) { + const scripts = [...shell.matchAll(scriptRe)].map(m => m[1]); + const stripped = shell.replace(scriptRe, ""); + const parsed = new DOMParser().parseFromString(stripped, "text/html"); + // The root carries a hydration key (_hk) the app claims — copy its + // attributes, not just its contents. + for (const attr of [...parsed.documentElement.attributes]) { + document.documentElement.setAttribute(attr.name, attr.value); + } + document.documentElement.innerHTML = parsed.documentElement.innerHTML; + for (const s of scripts) (0, eval)(s); +} + +function applyRest(rest: string) { + const scripts = [...rest.matchAll(scriptRe)].map(m => m[1]); + const stripped = rest.replace(scriptRe, ""); + if (stripped.trim()) document.body.insertAdjacentHTML("beforeend", stripped); + for (const s of scripts) (0, eval)(s); +} + +async function settle() { + await sleep(30); + flush(); + await sleep(30); + flush(); +} + +async function runScenario(scenario: Scenario) { + const { shell, rest } = loadArtifact(scenario.name); + (globalThis as any)._$HY = { events: [], completed: new WeakSet(), r: {}, fe() {} }; + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + let dispose: (() => void) | undefined; + + try { + applyShell(shell); + if (rest) applyRest(rest); + + // Server-rendered nodes must survive hydration with identity intact. + const serverNodes = [...document.body.querySelectorAll("p")]; + const serverTitle = document.head.querySelector("title"); + expect(serverTitle).not.toBe(null); + + dispose = hydrate(() => , document); + flush(); + await sleep(10); + flush(); + if (scenario.async) await settle(); + // head registry applies on a microtask (with hydration retries on macrotasks) + await sleep(10); + + expect(document.title).toBe(scenario.expectedTitle); + expect(document.head.querySelectorAll("title").length).toBe(1); + expect(document.body.textContent).toBe(scenario.expectedText); + + const afterNodes = [...document.body.querySelectorAll("p")]; + expect(afterNodes.length).toBe(serverNodes.length); + for (let i = 0; i < serverNodes.length; i++) { + expect(afterNodes[i], `server

#${i} was replaced during hydration`).toBe(serverNodes[i]); + } + + if (scenario.update) { + scenario.update(); + flush(); + await sleep(10); + expect(document.title).toBe(scenario.expectedTitleAfterUpdate ?? scenario.expectedTitle); + expect(document.body.textContent).toBe( + scenario.expectedTextAfterUpdate ?? scenario.expectedText + ); + } + + expect(warn).not.toHaveBeenCalled(); + } finally { + warn.mockRestore(); + dispose?.(); + await sleep(0); + document.body.innerHTML = ""; + document.head.innerHTML = ""; + for (const attr of [...document.documentElement.attributes]) { + document.documentElement.removeAttribute(attr.name); + } + (globalThis as any)._$HY = undefined; + } +} + +describe("head parity harness — client hydrate", () => { + beforeEach(async () => { + await sleep(0); + }); + afterEach(async () => { + await sleep(0); + }); + + for (const scenario of scenarios) { + test(scenario.name, () => runScenario(scenario)); + } +}); diff --git a/test/server/head-harness.spec.tsx b/test/server/head-harness.spec.tsx new file mode 100644 index 0000000..2c28dc0 --- /dev/null +++ b/test/server/head-harness.spec.tsx @@ -0,0 +1,63 @@ +/** @jsxImportSource @solidjs/web */ + +// Server half of the head hydration parity harness. Renders every scenario +// from test/harness/scenarios.tsx with the ssr generate, asserts the head +// output, and writes chunk artifacts for test/hydration/head-parity.spec.tsx +// to replay against the dom-generate compilation of the same source. +import { describe, expect, test } from "vitest"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { renderToStream } from "@solidjs/web"; +import { scenarios } from "../harness/scenarios.jsx"; + +const artifactsDir = resolve(dirname(fileURLToPath(import.meta.url)), "../harness/__artifacts__"); +mkdirSync(artifactsDir, { recursive: true }); + +function collectChunks(code: () => any): Promise<{ shell: string; rest: string }> { + return new Promise(resolvePromise => { + const chunks: string[] = []; + let shell = ""; + let shellDone = false; + renderToStream(code, { + onCompleteShell() { + shellDone = true; + } + }).pipe({ + write(chunk: string) { + chunks.push(chunk); + if (shellDone && !shell) shell = chunks.join(""); + }, + end() { + const full = chunks.join(""); + if (!shell) shell = full; + resolvePromise({ shell, rest: full.slice(shell.length) }); + } + }); + }); +} + +describe("head parity harness — server render", () => { + for (const scenario of scenarios) { + test(scenario.name, async () => { + const { shell, rest } = await collectChunks(() => ); + const full = shell + rest; + // Root-level titles land in the shell head with their claim key; + // boundary-attributed ones ride the fragment stream as patch ops. + const asShellTag = shell.includes(`>${scenario.expectedTitle}`); + const asPatchOp = full.includes(`"t","${scenario.expectedTitle}"`); + expect(asShellTag || asPatchOp, "expected title in shell or as patch op").toBe(true); + if (asShellTag) expect(shell).toContain(`data-dh="title"`); + + const visible = full.replace(//g, "").replace(/<[^>]*>/g, ""); + for (const token of scenario.expectedText.split(/\s+/).filter(Boolean)) { + expect(visible).toContain(token); + } + + writeFileSync( + resolve(artifactsDir, `${scenario.name}.json`), + JSON.stringify({ name: scenario.name, shell, rest }, null, 2) + ); + }); + } +}); diff --git a/tsconfig.test.json b/tsconfig.test.json index c766415..476e2f4 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -2,6 +2,7 @@ "compilerOptions": { "noEmit": true, "target": "esnext", + "module": "esnext", "moduleResolution": "bundler", "strict": true, "lib": ["dom", "esnext", "dom.iterable"], diff --git a/vitest.config.hydrate.ts b/vitest.config.hydrate.ts new file mode 100644 index 0000000..f92ad0e --- /dev/null +++ b/vitest.config.hydrate.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vitest/config'; +import solidPlugin from 'vite-plugin-solid'; + +// Client half of the SSR -> hydrate parity harness: compiles the shared +// scenarios with the hydratable dom generate and replays the artifacts +// written by the server project (vitest.config.ssr.ts) into jsdom. +export default defineConfig({ + // hot: false — solid-refresh wraps top-level components in HMR wrappers + // that add owners and break hydration id parity. + plugins: [solidPlugin({ hot: false, solid: { hydratable: true } })], + test: { + environment: 'jsdom', + globals: true, + include: ['test/hydration/**/*.spec.{ts,tsx}'], + testTransformMode: { + web: ['/\.[jt]sx?$/'], + }, + setupFiles: ['./test/setup-vitest.js'], + deps: { + inline: [/solid-js/, /@solidjs\/web/, /@solidjs\/meta/] + } + }, + resolve: { + conditions: ['browser', 'development'] + } +}); diff --git a/vitest.config.ssr.ts b/vitest.config.ssr.ts new file mode 100644 index 0000000..b48678f --- /dev/null +++ b/vitest.config.ssr.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vitest/config'; +import solidPlugin from 'vite-plugin-solid'; +import { resolve } from 'node:path'; + +// Server half of the SSR -> hydrate parity harness: compiles the shared +// scenarios with the ssr generate and renders them with the node builds of +// solid-js/@solidjs/web. Runs before vitest.config.hydrate.ts (see the +// test:unit script) so artifacts are always regenerated from current source. +export default defineConfig({ + plugins: [solidPlugin({ hot: false, solid: { generate: 'ssr', hydratable: true } })], + test: { + environment: 'node', + globals: true, + include: ['test/server/**/*.spec.{ts,tsx}'], + setupFiles: ['./test/setup-vitest.js'], + deps: { + inline: [/solid-js/, /@solidjs\/web/, /@solidjs\/meta/] + } + }, + resolve: { + conditions: ['node'], + // vitest's ssr resolver keeps the `development` condition active, which + // outranks `node` here and hands back the browser dev build — pin the + // server builds directly. + alias: { + '@solidjs/web': resolve(__dirname, 'node_modules/@solidjs/web/dist/server.js'), + 'solid-js': resolve(__dirname, 'node_modules/solid-js/dist/server.js') + } + } +}); diff --git a/vitest.config.ts b/vitest.config.ts index 9072d41..a145583 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,6 +10,9 @@ export default defineConfig({ test: { environment: 'jsdom', globals: true, + // test/server and test/hydration belong to the parity harness projects + // (vitest.config.ssr.ts / vitest.config.hydrate.ts). + include: ['test/*.spec.{ts,tsx}'], testTransformMode: { web: ['/\.[jt]sx?$/'], }, From 558d750a4cae19faf7bdeae926c97339249cfdf9 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Fri, 7 Aug 2026 13:23:13 -0700 Subject: [PATCH 8/8] v1.0.0-next.1 Co-authored-by: Cursor --- .changeset/pre.json | 2 ++ CHANGELOG.md | 22 ++++++++++++++++++++++ package.json | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index dc32b87..677cf14 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -5,6 +5,8 @@ "@solidjs/meta": "0.30.0-next.0" }, "changesets": [ + "drop-usehead-reexport", + "fix-title-body-hydration-id-drift", "solid-meta-1-0" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bfac7e..c152209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # @solidjs/meta +## 1.0.0-next.1 + +### Patch Changes + +- 36a5a18: Drop the `useHead` / `HeadTag` re-exports. The primitive belongs to + `@solidjs/web` (a required peer) — import it from there. Keeping a second + import path for the identical function only invited confusion about whether + they differ. +- 8a74994: Fix hydration id drift from dynamic Title/Style/Script bodies + + Compiled JSX children (e.g. `Page {site()}`) can allocate a + computation when read. The head registry evaluates descriptor getters at + runtime-dependent points — the registry effect on the client, stream flush on + the server — so that allocation landed under different owners on each runtime + and drifted every hydration id assigned after it, breaking claims for content + rendered after the tag (most visibly under a streamed `Loading` boundary). + + `headTag` now pins the body in a memo created at registration, under the + component, at the same point on both runtimes; the descriptor getter is a pure + read. Adds an SSR -> hydrate parity harness covering sync, root-async, and + boundary-async Title bodies. + ## 1.0.0-next.0 ### Major Changes diff --git a/package.json b/package.json index 03442c3..def83bd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@solidjs/meta", "description": "Write meta tags to the document head", - "version": "1.0.0-next.0", + "version": "1.0.0-next.1", "author": "Ryan Carniato", "license": "MIT", "repository": {