diff --git a/docs/components/sandbox/signal-counter.css b/docs/components/sandbox/signal-counter.css new file mode 100644 index 00000000..e4993039 --- /dev/null +++ b/docs/components/sandbox/signal-counter.css @@ -0,0 +1,19 @@ +:host, +:root { + text-align: center; +} + +.heading { + display: block; + text-decoration: underline; +} + +.even { + color: green; + display: block; +} + +.odd { + color: red; + display: block; +} diff --git a/docs/components/sandbox/signal-counter.tsx b/docs/components/sandbox/signal-counter.tsx new file mode 100644 index 00000000..af8b687d --- /dev/null +++ b/docs/components/sandbox/signal-counter.tsx @@ -0,0 +1,62 @@ +import sheet from './signal-counter.css' with { type: 'css' }; + +export const inferredObservability = true; + +export default class SignalCounter extends HTMLElement { + count; + parity; + isLarge; + + constructor() { + super(); + this.count = new Signal.State(0); + this.parity = new Signal.Computed(() => (this.count.get() % 2 === 0 ? 'even' : 'odd')); + this.isLarge = new Signal.Computed(() => + this.count.get() >= 100 ? 'Wow!!!' : 'Keep Going...', + ); + } + + connectedCallback() { + if (!this.shadowRoot) { + this.attachShadow({ + mode: 'open', + }); + this.render(); + } + + this.shadowRoot.adoptedStyleSheets = [sheet]; + } + + increment() { + this.count.set(this.count.get() + 1); + } + + decrement() { + this.count.set(this.count.get() - 1); + } + + double() { + this.count.set(this.count.get() * 2); + } + + render() { + const { count, parity, isLarge } = this; + + return ( +
+ My Signal Counter + + + {/* TODO: inline version breaks with effects */} + {/* */} + + + The count is {count.get()} ({parity.get()}) + +

({isLarge.get()})

+
+ ); + } +} + +customElements.define('sb-signal-counter-jsx', SignalCounter); diff --git a/docs/pages/sandbox.html b/docs/pages/sandbox.html index 07f7c4e6..f52f3a8d 100644 --- a/docs/pages/sandbox.html +++ b/docs/pages/sandbox.html @@ -30,9 +30,15 @@ min-width: 5%; margin: 0 auto; } + + sb-signal-counter-jsx { + display: block; + margin: 0 auto; + width: 50%; + } - + + + + - +

WCC Sandbox

-

Light DOM (no JS)

+

JSX + inferredObservability

+ + +
+ + + + +
+      <sb-signal-counter-jsx></sb-signal-counter-jsx>
+    
+
+      <sb-signal-counter-jsx
+        count="3"
+      ></sb-signal-counter-jsx>
+    
+ + diff --git a/greenwood.config.ts b/greenwood.config.ts index 3a37489c..4329e4df 100644 --- a/greenwood.config.ts +++ b/greenwood.config.ts @@ -1,8 +1,33 @@ -import type { Config } from '@greenwood/cli'; +import type { Config, CopyPlugin } from '@greenwood/cli'; import { greenwoodPluginMarkdown } from '@greenwood/plugin-markdown'; import { greenwoodPluginImportJsx } from '@greenwood/plugin-import-jsx'; import { greenwoodPluginCssModules } from '@greenwood/plugin-css-modules'; import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; +import fs from 'node:fs'; + +// TODO: this does not run in dev :/ +function copyEffectPlugin(): CopyPlugin { + console.log('herere???'); + return { + type: 'copy', + name: 'plugin-copy-wcc-effect', + provider: async () => { + console.log('copy???'); + return [ + { + // copy a file + from: new URL('./src/effect.js', import.meta.url), + to: new URL('./node_modules/wc-compiler/src/effect.js', import.meta.url), + }, + ]; + }, + }; +} + +fs.copyFileSync( + new URL('./src/effect.js', import.meta.url), + new URL('./node_modules/wc-compiler/src/effect.js', import.meta.url), +); const config: Config = { activeContent: true, @@ -12,6 +37,7 @@ const config: Config = { importAttributes: ['css'], }, plugins: [ + // copyEffectPlugin(), greenwoodPluginImportRaw(), greenwoodPluginCssModules(), greenwoodPluginImportJsx(), diff --git a/package-lock.json b/package-lock.json index 17d0a40f..d02d07ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "wc-compiler", "version": "0.19.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@projectevergreen/acorn-jsx-esm": "~0.1.0", @@ -14,6 +15,7 @@ "acorn-walk": "^8.3.4", "astring": "^1.9.0", "parse5": "^7.2.1", + "signal-polyfill": "^0.2.2", "sucrase": "^3.35.0" }, "devDependencies": { @@ -42,6 +44,7 @@ "lint-staged": "^16.2.6", "mocha": "^9.2.2", "open-props": "^1.7.4", + "patch-package": "^8.0.1", "playwright": "^1.58.2", "prettier": "^3.6.2", "prism-themes": "^1.9.0", @@ -135,11 +138,14 @@ } }, "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/@cacheable/memory": { "version": "2.0.7", @@ -165,14 +171,14 @@ } }, "node_modules/@cacheable/utils": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.3.tgz", - "integrity": "sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.4.tgz", + "integrity": "sha512-knwKUJEYgIfwShABS1BX6JyJJTglAFcEU7EXqzTdiGCXur4voqkiJkdgZIQtWNFhynzDWERcTYv/sETMu3uJWA==", "dev": true, "license": "MIT", "dependencies": { "hashery": "^1.3.0", - "keyv": "^5.5.5" + "keyv": "^5.6.0" } }, "node_modules/@cacheable/utils/node_modules/keyv": { @@ -209,9 +215,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.26", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz", - "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==", + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", + "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", "dev": true, "funding": [ { @@ -293,9 +299,9 @@ } }, "node_modules/@double-great/stylelint-a11y": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@double-great/stylelint-a11y/-/stylelint-a11y-3.4.1.tgz", - "integrity": "sha512-Z9wH2Z2yXgTvztZsm4b9E2up5Ri6VNRg4dciGmZWejH3yO+d42prLwVbWoXIngc62gWTAymzMyAht1gkaZQ9Pw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@double-great/stylelint-a11y/-/stylelint-a11y-3.4.3.tgz", + "integrity": "sha512-AhAMyxl/w/wQvKdY1jdWDkF8BEgt6DKEZNGNyecJnKzY/MEq9iA1o63qwPtPTPHFN1jQjp5Vnq0fySxlfFw3fA==", "dev": true, "license": "MIT", "dependencies": { @@ -1991,9 +1997,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz", - "integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==", + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "dev": true, "license": "MIT", "dependencies": { @@ -2050,17 +2056,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -2073,8 +2079,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -2089,16 +2095,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "engines": { @@ -2109,19 +2115,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "engines": { @@ -2136,14 +2142,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2154,9 +2160,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", "dev": true, "license": "MIT", "engines": { @@ -2171,15 +2177,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -2191,14 +2197,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -2210,16 +2216,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", @@ -2264,16 +2270,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2283,19 +2289,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2305,6 +2311,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -2397,16 +2416,6 @@ } } }, - "node_modules/@vitest/coverage-v8/node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -2538,6 +2547,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -2893,13 +2909,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -2995,6 +3004,13 @@ "node": ">=0.8.0" } }, + "node_modules/babel-code-frame/node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true, + "license": "MIT" + }, "node_modules/babel-code-frame/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -3230,6 +3246,13 @@ "node": ">=10.12.0" } }, + "node_modules/c8/node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/cacheable": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.2.tgz", @@ -3316,6 +3339,25 @@ "@keyv/serialize": "^1.1.1" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -3530,11 +3572,20 @@ } }, "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, - "license": "MIT" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/cli-boxes": { "version": "2.2.1", @@ -4037,13 +4088,13 @@ } }, "node_modules/css-functions-list": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", - "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12 || >=16" + "node": ">=12" } }, "node_modules/css-select": { @@ -4289,6 +4340,24 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5118,6 +5187,13 @@ "node": ">=0.10.0" } }, + "node_modules/expand-range/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/expand-range/node_modules/isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", @@ -5364,6 +5440,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -5563,9 +5649,9 @@ } }, "node_modules/geist": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/geist/-/geist-1.5.1.tgz", - "integrity": "sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/geist/-/geist-1.7.0.tgz", + "integrity": "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==", "dev": true, "license": "SIL OPEN FONT LICENSE", "peerDependencies": { @@ -5583,9 +5669,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "dev": true, "license": "MIT", "engines": { @@ -5836,6 +5922,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/globjoin": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", @@ -5993,6 +6089,19 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -6033,9 +6142,9 @@ } }, "node_modules/hashery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.4.0.tgz", - "integrity": "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.0.tgz", + "integrity": "sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6968,6 +7077,13 @@ "is-ci": "bin.js" } }, + "node_modules/is-ci/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7005,6 +7121,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -7222,6 +7354,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-yarn-global": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", @@ -7230,9 +7375,9 @@ "license": "MIT" }, "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, @@ -7293,9 +7438,9 @@ } }, "node_modules/js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -7387,6 +7532,26 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -7404,6 +7569,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -7440,6 +7615,16 @@ "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -10440,6 +10625,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -10503,6 +10698,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open-props": { "version": "1.7.23", "resolved": "https://registry.npmjs.org/open-props/-/open-props-1.7.23.tgz", @@ -10779,6 +10991,84 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/patch-package/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/patch-package/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/patch-package/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11226,9 +11516,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11372,6 +11662,13 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -12131,9 +12428,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -12190,6 +12487,24 @@ "dev": true, "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-getter": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz", @@ -12330,6 +12645,12 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/signal-polyfill/-/signal-polyfill-0.2.2.tgz", + "integrity": "sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==", + "license": "Apache-2.0" + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -12346,13 +12667,13 @@ } }, "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, "node_modules/slice-ansi": { @@ -12386,11 +12707,14 @@ } }, "node_modules/smob": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", + "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/smpltmpl": { "version": "1.0.2", @@ -12519,14 +12843,14 @@ } }, "node_modules/string-width": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", - "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { "node": ">=20" @@ -12909,9 +13233,9 @@ } }, "node_modules/table/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -13434,16 +13758,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", - "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.54.0", - "@typescript-eslint/parser": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0" + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -13453,7 +13777,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, diff --git a/package.json b/package.json index 1eaf68ca..851b3ce3 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "./register": "./src/register.js", "./dom-shim": "./src/dom-shim.js", "./jsx-loader": "./src/jsx-loader.js", - "./jsx-runtime": "./src/jsx-runtime.ts" + "./jsx-runtime": "./src/jsx-runtime.ts", + "./effect": "./src/effect.js" }, "author": "Owen Buckley ", "keywords": [ @@ -53,7 +54,8 @@ "test:tdd:jsx": "npm run test:jsx -- --watch", "test:docs": "vitest run --coverage", "test:docs:tdd": "vitest", - "prepare": "husky" + "prepare": "husky", + "postinstall": "patch-package" }, "dependencies": { "@projectevergreen/acorn-jsx-esm": "~0.1.0", @@ -61,6 +63,7 @@ "acorn-walk": "^8.3.4", "astring": "^1.9.0", "parse5": "^7.2.1", + "signal-polyfill": "^0.2.2", "sucrase": "^3.35.0" }, "devDependencies": { @@ -89,6 +92,7 @@ "lint-staged": "^16.2.6", "mocha": "^9.2.2", "open-props": "^1.7.4", + "patch-package": "^8.0.1", "playwright": "^1.58.2", "prettier": "^3.6.2", "prism-themes": "^1.9.0", diff --git a/patches/@greenwood+cli+0.34.0-alpha.4.patch b/patches/@greenwood+cli+0.34.0-alpha.4.patch new file mode 100644 index 00000000..a68b79f3 --- /dev/null +++ b/patches/@greenwood+cli+0.34.0-alpha.4.patch @@ -0,0 +1,10 @@ +diff --git a/node_modules/@greenwood/cli/src/lib/execute-route-module.js b/node_modules/@greenwood/cli/src/lib/execute-route-module.js +index a7367ac..570f2f7 100644 +--- a/node_modules/@greenwood/cli/src/lib/execute-route-module.js ++++ b/node_modules/@greenwood/cli/src/lib/execute-route-module.js +@@ -1,4 +1,4 @@ +-import { renderToString, renderFromHTML } from "wc-compiler"; ++import { renderToString, renderFromHTML } from "../../../../../src/wcc.js"; + + async function executeRouteModule({ + moduleUrl, diff --git a/patches/@greenwood+plugin-import-jsx+0.34.0-alpha.4.patch b/patches/@greenwood+plugin-import-jsx+0.34.0-alpha.4.patch new file mode 100644 index 00000000..d2696cc8 --- /dev/null +++ b/patches/@greenwood+plugin-import-jsx+0.34.0-alpha.4.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@greenwood/plugin-import-jsx/src/index.js b/node_modules/@greenwood/plugin-import-jsx/src/index.js +index 5fe6e11..5975de3 100644 +--- a/node_modules/@greenwood/plugin-import-jsx/src/index.js ++++ b/node_modules/@greenwood/plugin-import-jsx/src/index.js +@@ -4,7 +4,7 @@ + * + */ + import { generate } from "astring"; +-import { parseJsx } from "wc-compiler/src/jsx-loader.js"; ++import { parseJsx } from "../../../../src/jsx-loader.js"; + + class ImportJsxResource { + constructor(compilation, options) { diff --git a/src/dom-shim.js b/src/dom-shim.js index 01be10bd..a95c654b 100644 --- a/src/dom-shim.js +++ b/src/dom-shim.js @@ -149,6 +149,10 @@ class Node extends EventTarget { this.childNodes.push(textNode); } } + + // minimal shim to support JSX <> Signals compilation and caching DOM references tracked to effects + querySelector() {} + querySelectorAll() {} } // https://developer.mozilla.org/en-US/docs/Web/API/Element diff --git a/src/effect.js b/src/effect.js new file mode 100644 index 00000000..5c70e357 --- /dev/null +++ b/src/effect.js @@ -0,0 +1,39 @@ +// https://github.com/proposal-signals/signal-polyfill?tab=readme-ov-file#creating-a-simple-effect +import { Signal } from 'signal-polyfill'; + +let needsEnqueue = true; + +const w = new Signal.subtle.Watcher(() => { + if (needsEnqueue) { + needsEnqueue = false; + queueMicrotask(processPending); + } +}); + +function processPending() { + needsEnqueue = true; + + for (const s of w.getPending()) { + s.get(); + } + + w.watch(); +} + +export function effect(callback) { + let cleanup; + + const computed = new Signal.Computed(() => { + typeof cleanup === 'function' && cleanup(); + cleanup = callback(); + }); + + w.watch(computed); + computed.get(); + + return () => { + w.unwatch(computed); + typeof cleanup === 'function' && cleanup(); + cleanup = undefined; + }; +} diff --git a/src/jsx-loader.js b/src/jsx-loader.js index 55b17e20..2c14ecbe 100644 --- a/src/jsx-loader.js +++ b/src/jsx-loader.js @@ -6,19 +6,27 @@ import fs from 'fs'; // ideally we can eventually adopt an ESM compatible version of this plugin // https://github.com/acornjs/acorn-jsx/issues/112 // @ts-ignore -// but it does have a default export??? +// but it does have a default export? import jsx from '@projectevergreen/acorn-jsx-esm'; import { parse, parseFragment, serialize } from 'parse5'; import { transform } from 'sucrase'; +// Signal has to come before effect +import { Signal } from 'signal-polyfill'; +globalThis.Signal = Signal; + +// no-op implementation for SSR +function effect() {} +globalThis.effect = effect; + const jsxRegex = /\.(jsx)$/; const tsxRegex = /\.(tsx)$/; -// TODO same hack as definitions +// TODO: same hack as definitions // https://github.com/ProjectEvergreen/wcc/discussions/74 let string; -// TODO move to a util +// TODO: move to a util // https://github.com/ProjectEvergreen/wcc/discussions/74 function getParse(html) { return html.indexOf('') >= 0 || html.indexOf('') >= 0 || html.indexOf('') >= 0 @@ -66,6 +74,9 @@ function applyDomDepthSubstitutions(tree, currentDepth = 1, hasShadowRoot = fals } } + // TODO: handle text nodes for __this__ references + // https://github.com/ProjectEvergreen/wcc/issues/88 + if (node.childNodes && node.childNodes.length > 0) { applyDomDepthSubstitutions(node, currentDepth + 1, hasShadowRoot); } @@ -77,7 +88,47 @@ function applyDomDepthSubstitutions(tree, currentDepth = 1, hasShadowRoot = fals return tree; } -function parseJsxElement(element, moduleContents = '', inferredObservability = false) { +// TODO handle if / else statements +// https://github.com/ProjectEvergreen/wcc/issues/88 +function findThisReferences(context, statement) { + const references = []; + const isRenderFunctionContext = context === 'render'; + const { expression, type } = statement; + const isConstructorThisAssignment = + context === 'constructor' && + type === 'ExpressionStatement' && + expression.type === 'AssignmentExpression' && + expression.left.object.type === 'ThisExpression'; + + if (isConstructorThisAssignment) { + // this.name = 'something'; // constructor + references.push(expression.left.property.name); + } else if (isRenderFunctionContext && type === 'VariableDeclaration') { + statement.declarations.forEach((declaration) => { + const { init, id } = declaration; + + if (init.object && init.object.type === 'ThisExpression') { + // const { description } = this.todo; + references.push(init.property.name); + } else if (init.type === 'ThisExpression' && id && id.properties) { + // const { id, description } = this; + id.properties.forEach((property) => { + references.push(property.key.name); + }); + } else { + // TODO: we are just blindly tracking anything here. + // everything should ideally be mapped to actual this references, to create a strong chain of direct reactivity + // instead of tracking any declaration as a derived tracking attr + // for convenience here, we push the entire declaration here, instead of the name like for direct this references (see above) + references.push(declaration); + } + }); + } + + return references; +} + +function parseJsxElement(element, moduleContents = '', inferredObservability) { try { const { type } = element; @@ -124,25 +175,38 @@ function parseJsxElement(element, moduleContents = '', inferredObservability = f if (left.object.type === 'ThisExpression') { if (left.property.type === 'Identifier') { - if (inferredObservability) { - // very naive (fine grained?) reactivity - // string += ` ${name}="__this__.${left.property.name}${expression.operator}${right.raw}; __this__.update(\\'${left.property.name}\\', null, __this__.${left.property.name});"`; - string += ` ${name}="__this__.${left.property.name}${expression.operator}${right.raw}; __this__.setAttribute(\\'${left.property.name}\\', __this__.${left.property.name});"`; - } else { - // implicit reactivity using this.render - string += ` ${name}="__this__.${left.property.name}${expression.operator}${right.raw}; __this__.render();"`; - } + // implicit reactivity using this.render + string += ` ${name}="__this__.${left.property.name}${expression.operator}${right.raw}; __this__.render();"`; } } } } } else if (attribute.name.type === 'JSXIdentifier') { - // TODO is there any difference between an attribute for an event handler vs a normal attribute? - // Can all these be parsed using one function> if (attribute.value) { + const expression = attribute?.value?.expression; if (attribute.value.type === 'Literal') { // xxx="yyy" > string += ` ${name}="${attribute.value.value}"`; + } else if ( + expression && + inferredObservability && + attribute.value.type === 'JSXExpressionContainer' && + expression?.type === 'CallExpression' && + expression?.callee.type === 'MemberExpression' && + expression?.arguments && + expression?.callee?.property?.name === 'get' + ) { + // xxx={products.get().length} > + // TODO: do we need to handle for set()? + const { object, property } = expression.callee; + + if (object.type === 'MemberExpression' && object?.object.type === 'ThisExpression') { + // The count is counter={this.count.get()} + string += ` ${name}=$\{${object.property.name}.${property.name}()}`; + } else if (object.type === 'Identifier') { + // xxx={products.get().length} > + string += ` ${name}=$\{${object.name}.${property.name}()}`; + } } else if (attribute.value.type === 'JSXExpressionContainer') { // xxx={allTodos.length} > const { value } = attribute; @@ -166,11 +230,6 @@ function parseJsxElement(element, moduleContents = '', inferredObservability = f default: break; } - - // only apply this when dealing with `this` references - if (inferredObservability) { - string += ` data-wcc-${expression.name}="${name}" data-wcc-ins="attr"`; - } } } else { // xxx > @@ -197,15 +256,20 @@ function parseJsxElement(element, moduleContents = '', inferredObservability = f if (type === 'JSXExpressionContainer') { const { type } = element.expression; - if (type === 'Identifier') { + if ( + inferredObservability && + type === 'CallExpression' && + element.expression.arguments && + element.expression?.callee?.type === 'MemberExpression' && + element.expression?.callee?.property?.name === 'get' + ) { + // TODO: handle this references + // https://github.com/ProjectEvergreen/wcc/issues/88 + // TODO: do we need to handle for set()? + const { object, property } = element.expression.callee; + string += `$\{${object.name}.${property.name}()}`; + } else if (type === 'Identifier') { // You have {count} TODOs left to complete - if (inferredObservability) { - const { name } = element.expression; - - string = `${string.slice(0, string.lastIndexOf('>'))} data-wcc-${name}="\${this.${name}}" data-wcc-ins="text">`; - } - // TODO be able to remove this extra data attribute - // string = `${string.slice(0, string.lastIndexOf('>'))} data-wcc-${name} data-wcc-ins="text">`; string += `$\{${element.expression.name}}`; } else if (type === 'MemberExpression') { const { object } = element.expression.object; @@ -230,44 +294,33 @@ function parseJsxElement(element, moduleContents = '', inferredObservability = f return string; } -// TODO handle if / else statements -// https://github.com/ProjectEvergreen/wcc/issues/88 -function findThisReferences(context, statement) { - const references = []; - const isRenderFunctionContext = context === 'render'; - const { expression, type } = statement; - const isConstructorThisAssignment = - context === 'constructor' && - type === 'ExpressionStatement' && - expression.type === 'AssignmentExpression' && - expression.left.object.type === 'ThisExpression'; +function generateEffectsForReactiveElement(reactiveElement, idx) { + const { effect, attributes } = reactiveElement; + let contents = ''; - if (isConstructorThisAssignment) { - // this.name = 'something'; // constructor - references.push(expression.left.property.name); - } else if (isRenderFunctionContext && type === 'VariableDeclaration') { - statement.declarations.forEach((declaration) => { - const { init, id } = declaration; + if (reactiveElement.effect?.expression) { + contents += ` + effect(() => { + this.$el${idx}.textContent = ${effect.expression} + });\n + `; + } - if (init.object && init.object.type === 'ThisExpression') { - // const { description } = this.todo; - references.push(init.property.name); - } else if (init.type === 'ThisExpression' && id && id.properties) { - // const { id, description } = this; - id.properties.forEach((property) => { - references.push(property.key.name); - }); - } else { - // TODO we are just blindly tracking anything here. - // everything should ideally be mapped to actual this references, to create a strong chain of direct reactivity - // instead of tracking any declaration as a derived tracking attr - // for convenience here, we push the entire declaration here, instead of the name like for direct this references (see above) - references.push(declaration); - } - }); + if (attributes.length > 0) { + const attributeUpdates = attributes + .map((attr) => { + return `this.$el${idx}.setAttribute('${attr.name}', this.${attr.value}.get())`; + }) + .join('\n'); + + contents += ` + effect(() => { + ${attributeUpdates} + });\n + `; } - return references; + return contents; } export function parseJsx(moduleURL) { @@ -281,14 +334,23 @@ export function parseJsx(moduleURL) { // however, this requires making parseJsx async, but WCC acorn walking is done sync const hasOwnObservedAttributes = undefined; let inferredObservability = false; + // TODO: "merge" observedAttributes tracking with constructor tracking let observedAttributes = []; + let constructorMembersSignals = new Map(); + let reactiveElements = []; + let componentName; + let hasShadowRoot; let tree = acorn.Parser.extend(jsx()).parse(result.code, { ecmaVersion: 'latest', sourceType: 'module', }); string = ''; - // TODO: would be nice to do this one pass, but first we need to know if `inferredObservability` is set first + // initial pass to get certain information before running JSX transformations (could we do this in one pass?) + // 1. if `inferredObservability` is set + // 2. get the name of the component class for `static` references + // 3, track observed attributes from `this` references in the template + // 4. track if Shadow DOM is being used walk.simple( tree, { @@ -308,6 +370,79 @@ export function parseJsx(moduleURL) { } } }, + ExportDefaultDeclaration(node) { + const { declaration } = node; + + if ( + declaration && + declaration.type === 'ClassDeclaration' && + declaration.id && + declaration.id.name + ) { + componentName = declaration.id.name; + } + }, + ClassDeclaration(node) { + // @ts-ignore + if (node.superClass.name === 'HTMLElement') { + // TODO: (good first issue) find a more AST (visitor) based way to check for this + hasShadowRoot = + moduleContents.slice(node.body.start, node.body.end).indexOf('this.attachShadow(') > 0; + + for (const n1 of node.body.body) { + if (n1.type === 'MethodDefinition') { + // @ts-ignore + const nodeName = n1.key.name; + if (nodeName === 'render') { + for (const n2 in n1.value.body.body) { + const n = n1.value.body.body[n2]; + + if (n.type === 'VariableDeclaration') { + observedAttributes = [ + ...observedAttributes, + ...findThisReferences('render', n), + ]; + } + } + } + } + } + } + }, + MethodDefinition(node) { + // @ts-ignore + if ( + node.kind === 'constructor' && + node?.value.type === 'FunctionExpression' && + node.value.body?.type === 'BlockStatement' + ) { + const root = node.value.body?.body; + for (const n of root) { + if ( + n.type === 'ExpressionStatement' && + n.expression?.type === 'AssignmentExpression' && + n.expression?.operator === '=' && + n.expression.left.object.type === 'ThisExpression' + ) { + const { left, right } = n.expression; + if ( + right.type === 'NewExpression' && + right.callee?.object?.type === 'Identifier' && + right.callee?.object?.name === 'Signal' + ) { + const name = left.property.name; + const isState = right.callee?.property?.name === 'State'; + const isComputed = right.callee?.property?.name === 'Computed'; + + constructorMembersSignals.set(name, { + isState, + isComputed, + }); + } + } + } + } + }, }, { // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171 @@ -317,15 +452,146 @@ export function parseJsx(moduleURL) { }, ); + // we do this first to track reactivity usage before we transform the template and re-write its contents + if (inferredObservability && observedAttributes.length > 0 && !hasOwnObservedAttributes) { + console.log('inferredObservability enabled. scanning for reactivity...', observedAttributes); + + // this scans for signals usage within the template and builds up a reactive list of templates + effects + // (could this be done during the transformation pass instead of having to generate and re-parse the module again?) + // - TODO: recursive scanning for nested components + walk.simple( + tree, + { + MethodDefinition(node) { + if (node.key.name === 'render') { + for (const n2 in node.value.body.body) { + const n = node.value.body.body[n2]; + if (n.type === 'ReturnStatement' && n.argument.type === 'JSXElement') { + const parentTag = n.argument.openingElement.name.name; + console.log('ENTERING TEMPLATE AT =>', { parentTag }); + const children = []; + + for (const child of n.argument.children) { + // TODO: need to handle recursion + if (child.type === 'JSXText') { + // console.log('TEXT NODE', { child }) + // TODO: track top level reactivity for text nodes? + } else if (child.type === 'JSXElement') { + const childTag = child.openingElement.name.name; + console.log('ENTERING CHILD ELEMENT', { childTag }); + + // track children for determining correct effect selector + if (!children[childTag]) { + children[childTag] = []; + } + children[childTag].push(childTag); + + // TODO: I think we are only checking for State, I think we also need to handle Computeds (by themselves) here as well + // TODO: should we filter the things that call .get() out against actual signals found in the constructor / JSX? + const hasReactiveTemplate = child.children.some( + (c) => + c.type === 'JSXExpressionContainer' && + c.expression?.type === 'CallExpression' && + c.expression?.callee?.type === 'MemberExpression' && + c.expression?.callee?.property?.name === 'get', + ); + const hasReactiveAttributes = child.openingElement.attributes.some( + (a) => + a.value.type === 'JSXExpressionContainer' && + a.value.expression?.type === 'CallExpression' && + a.value.expression?.callee?.type === 'MemberExpression' && + a.value.expression?.callee?.property?.name === 'get', + ); + + if (hasReactiveTemplate || hasReactiveAttributes) { + reactiveElements.push({ + selector: `${parentTag} > ${childTag}:nth-of-type(${children[childTag].length})`, + template: {}, + attributes: [], + }); + } + + if (hasReactiveTemplate) { + console.log('CHILD ELEMENT HAS TEXT CONTENT REACTIVITY', { childTag }); + const signals = []; + let template = ''; + + for (const c of child.children) { + if (c.type === 'JSXText') { + template += c.value; + } else if (c.type === 'JSXExpressionContainer') { + // TODO: handle this references? + // https://github.com/ProjectEvergreen/wcc/issues/88 + const { object } = c.expression.callee || {}; + template += `$\{${object.name}}`; + signals.push(object.name); + } + } + + if (template !== '' && signals.length > 0) { + const $$templ = `$$tmpl${reactiveElements.length - 1}`; + // TODO: need to handle runtime assumption here with `_wcc`, or do we even need it all? I don't think so... + const staticTemplate = `static ${$$templ} = (${signals.join(',')}) => _wcc\`${template.trim()}\`;`; + // TODO: handle this references? + // https://www.github.com/ProjectEvergreen/wcc/issues/88 + const expression = `${componentName}.${$$templ}(${signals.map((s) => `this.${s}.get()`).join(', ')});`; + + reactiveElements[reactiveElements.length - 1].effect = { + template: staticTemplate, + expression, + }; + } + } + + if (hasReactiveAttributes) { + console.log( + 'CHILD ELEMENT HAS ATTRIBUTE REACTIVITY', + child.openingElement.name.name, + ); + for (const attr of child.openingElement.attributes) { + if ( + attr.value.type === 'JSXExpressionContainer' && + attr.value.expression?.type === 'CallExpression' && + attr.value.expression?.callee?.type === 'MemberExpression' && + attr.value.expression?.callee?.property?.name === 'get' + ) { + const isThisExpression = + attr.value.expression?.callee?.object?.object?.type === + 'ThisExpression'; + const value = isThisExpression + ? attr.value.expression?.callee?.object?.property.name + : attr.value.expression?.callee?.object.name; + + reactiveElements[reactiveElements.length - 1].attributes.push({ + name: attr.name.name, + value, + }); + } + } + } + } + } + } + } + } + }, + }, + { + // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171 + ...walk.base, + // @ts-ignore + JSXElement: () => {}, + }, + ); + } + + // apply all JSX transformations walk.simple( tree, { ClassDeclaration(node) { // @ts-ignore if (node.superClass.name === 'HTMLElement') { - const hasShadowRoot = - moduleContents.slice(node.body.start, node.body.end).indexOf('this.attachShadow(') > 0; - for (const n1 of node.body.body) { if (n1.type === 'MethodDefinition') { // @ts-ignore @@ -334,13 +600,7 @@ export function parseJsx(moduleURL) { for (const n2 in n1.value.body.body) { const n = n1.value.body.body[n2]; - if (n.type === 'VariableDeclaration') { - observedAttributes = [ - ...observedAttributes, - ...findThisReferences('render', n), - ]; - // @ts-ignore - } else if (n.type === 'ReturnStatement' && n.argument.type === 'JSXElement') { + if (n.type === 'ReturnStatement' && n.argument.type === 'JSXElement') { const html = parseJsxElement(n.argument, moduleContents, inferredObservability); const elementTree = getParse(html)(html); const elementRoot = hasShadowRoot ? 'this.shadowRoot' : 'this'; @@ -389,113 +649,101 @@ export function parseJsx(moduleURL) { }, ); + // TODO: why does this compilation run twice? logging here will output the message twice if (inferredObservability && observedAttributes.length > 0 && !hasOwnObservedAttributes) { - let insertPoint; - for (const line of tree.body) { - // TODO: test for class MyComponent vs export default class MyComponent - // @ts-ignore - if ( - line.type === 'ClassDeclaration' || - (line.declaration && line.declaration.type) === 'ClassDeclaration' - ) { - // @ts-ignore - insertPoint = line.declaration.body.start + 1; - } - } - - let newModuleContents = generate(tree); - const trackingAttrs = observedAttributes.filter((attr) => typeof attr === 'string'); - // TODO ideally derivedAttrs would explicitly reference trackingAttrs - // and if there are no derivedAttrs, do not include the derivedGetters / derivedSetters code in the compiled output - const derivedAttrs = observedAttributes.filter((attr) => typeof attr !== 'string'); - const derivedGetters = derivedAttrs - .map((attr) => { - return ` - get_${attr.id.name}(${trackingAttrs.join(',')}) { - return ${moduleContents.slice(attr.init.start, attr.init.end)} - } - `; - }) - .join('\n'); - const derivedSetters = derivedAttrs - .map((attr) => { - const name = attr.id.name; + console.log('inferredObservability enabled. applying templates and effects...', { + reactiveElements, + }); - return ` - const old_${name} = this.get_${name}(oldValue); - const new_${name} = this.get_${name}(newValue); - this.update('${name}', old_${name}, new_${name}); - `; - }) - .join('\n'); + // here we do the following with all the work we've done so far tracking attributes, signals, effects, etc + // 1. setup static template functions and observed attributes that we've tracked so far to inject into the top of the class body + // 2. append all effects to the end of the connectedCallback function + // 3. setup cache references to all elements used in effects + walk.simple( + tree, + { + ClassDeclaration(node) { + if ( + node.id.name === componentName && + node.type === 'ClassDeclaration' && + node.body.type === 'ClassBody' + ) { + // TODO: do we even need this filter? + const trackingAttrs = observedAttributes.filter((attr) => typeof attr === 'string'); + + console.log('effect', { reactiveElements }); + console.log('observedAttributes', trackingAttrs); + + // TODO: better way to determine value type, e,g. array, number, object, etc for `parseAttribute`? + // have to wrap these `static` calls in a class here, otherwise we can't parse them standalone w/ acorn + const staticContents = ` + class Stub { + ${reactiveElements.map((el, idx) => `$el${idx};`).join('')} + ${reactiveElements + .filter((el) => el.effect?.template) + .map((el) => el.effect.template) + .join('\n')} + static get observedAttributes() { + return [${[...trackingAttrs] + .filter((attr) => constructorMembersSignals.get(attr)?.isState) + .map((attr) => `'${attr}'`) + .join()}] + } + static parseAttribute = (value) => value.charAt(0) === '{' || value.charAt(0) === '[' + ? JSON.parse(value) + : !isNaN(value) + ? parseInt(value, 10) + : value === 'true' || value === 'false' + ? value === 'true' ? true : false + : value; + attributeChangedCallback(name, oldValue, newValue) { + this[name].set(${componentName}.parseAttribute(newValue)); + } + } + `; - // TODO: better way to determine value type, e,g. array, int, object, etc? - // TODO: better way to test for shadowRoot presence when running querySelectorAll - newModuleContents = `${newModuleContents.slice(0, insertPoint)} - static get observedAttributes() { - return [${[...trackingAttrs].map((attr) => `'${attr}'`).join()}] - } + const staticContentsTree = acorn.parse(staticContents, { + ecmaVersion: 'latest', + sourceType: 'module', + }); - attributeChangedCallback(name, oldValue, newValue) { - function getValue(value) { - return value.charAt(0) === '{' || value.charAt(0) === '[' - ? JSON.parse(value) - : !isNaN(value) - ? parseInt(value, 10) - : value === 'true' || value === 'false' - ? value === 'true' ? true : false - : value; - } - if (newValue !== oldValue) { - switch(name) { - ${trackingAttrs - .map((attr) => { - return ` - case '${attr}': - this.${attr} = getValue(newValue); - break; - `; - }) - .join('\n')} + node.body.body.unshift(...staticContentsTree.body[0].body.body); } - this.update(name, oldValue, newValue); - } - } - - update(name, oldValue, newValue) { - const attr = \`data-wcc-\${name}\`; - const selector = \`[\${attr}]\`; - - (this?.shadowRoot || this).querySelectorAll(selector).forEach((el) => { - // handle empty strings as a value for the purposes of attribute change detection - const needle = oldValue === '' ? '' : oldValue ?? el.getAttribute(attr); - - switch(el.getAttribute('data-wcc-ins')) { - case 'text': - el.textContent = el.textContent.replace(needle, newValue); - break; - case 'attr': - if (el.hasAttribute(el.getAttribute(attr))) { - el.setAttribute(el.getAttribute(attr), newValue); - } - break; + }, + MethodDefinition(node) { + if (node.key.name === 'connectedCallback') { + const root = hasShadowRoot ? 'this.shadowRoot' : 'this'; + const effectElements = reactiveElements + .map((el, idx) => `this.$el${idx} = ${root}.querySelector('${el.selector}');`) + .join('\n'); + const effectContents = reactiveElements + .map((element, idx) => generateEffectsForReactiveElement(element, idx)) + .join('\n'); + + const effectElementsTree = acorn.parse(effectElements, { + ecmaVersion: 'latest', + sourceType: 'module', + }); + const effectContentsTree = acorn.parse(effectContents, { + ecmaVersion: 'latest', + sourceType: 'module', + }); + + node.value.body.body = [ + ...node.value.body.body, + ...effectElementsTree.body, + ...effectContentsTree.body, + ]; } - }) - - if ([${[...trackingAttrs].map((attr) => `'${attr}'`).join()}].includes(name)) { - ${derivedSetters} - } - } - - ${derivedGetters} - - ${newModuleContents.slice(insertPoint)} - `; - - tree = acorn.Parser.extend(jsx()).parse(newModuleContents, { - ecmaVersion: 'latest', - sourceType: 'module', - }); + }, + }, + { + // https://github.com/acornjs/acorn/issues/829#issuecomment-1172586171 + ...walk.base, + // @ts-ignore + JSXElement: () => {}, + }, + ); } return tree; diff --git a/test/cases/jsx-inferred-observability/fixtures/attribute-changed-callback.txt b/test/cases/jsx-inferred-observability/fixtures/attribute-changed-callback.txt index ccc626bd..d4ac4441 100644 --- a/test/cases/jsx-inferred-observability/fixtures/attribute-changed-callback.txt +++ b/test/cases/jsx-inferred-observability/fixtures/attribute-changed-callback.txt @@ -1,40 +1,10 @@ +static parseAttribute = value => value.charAt(0) === '{' || value.charAt(0) === '[' + ? JSON.parse(value) + : !isNaN(value) + ? parseInt(value, 10) + : value === 'true' || value === 'false' + ? value === 'true' ? true : false + : value; attributeChangedCallback(name, oldValue, newValue) { - function getValue(value) { - return value.charAt(0) === '{' || value.charAt(0) === '[' ? JSON.parse(value) : !isNaN(value) ? parseInt(value, 10) : value === 'true' || value === 'false' ? value === 'true' ? true : false : value; - } - if (newValue !== oldValue) { - switch (name) { - case 'count': - this.count = getValue(newValue); - break; - case 'highlight': - this.highlight = getValue(newValue); - break; - } - this.update(name, oldValue, newValue); - } - } - update(name, oldValue, newValue) { - const attr = `data-wcc-${ name }`; - const selector = `[${ attr }]`; - - (this?.shadowRoot || this).querySelectorAll(selector).forEach(el => { - const needle = oldValue === '' ? '' : oldValue ?? el.getAttribute(attr); - - switch (el.getAttribute('data-wcc-ins')) { - case 'text': - el.textContent = el.textContent.replace(needle, newValue); - break; - case 'attr': - if (el.hasAttribute(el.getAttribute(attr))) { - el.setAttribute(el.getAttribute(attr), newValue); - } - break; - } - }); - if ([ - 'count', - 'highlight' - ].includes(name)) { - } - } \ No newline at end of file + this[name].set(Counter.parseAttribute(newValue)); +} \ No newline at end of file diff --git a/test/cases/jsx-inferred-observability/fixtures/effects.txt b/test/cases/jsx-inferred-observability/fixtures/effects.txt new file mode 100644 index 00000000..12e6ee0b --- /dev/null +++ b/test/cases/jsx-inferred-observability/fixtures/effects.txt @@ -0,0 +1,15 @@ +this.$el0 = this.shadowRoot.querySelector('div > wcc-badge:nth-of-type(1)'); +this.$el1 = this.shadowRoot.querySelector('div > span:nth-of-type(1)'); +this.$el2 = this.shadowRoot.querySelector('div > span:nth-of-type(4)'); +effect(() => { + this.$el0.setAttribute('count', this.count.get()); +}); +effect(() => { + this.$el1.textContent = Counter.$$tmpl1(this.count.get()); +}); +effect(() => { + this.$el1.setAttribute('data-count', this.count.get()); +}); +effect(() => { + this.$el2.textContent = Counter.$$tmpl2(this.parity.get()); +}); \ No newline at end of file diff --git a/test/cases/jsx-inferred-observability/fixtures/static-templates.txt b/test/cases/jsx-inferred-observability/fixtures/static-templates.txt new file mode 100644 index 00000000..888c44c9 --- /dev/null +++ b/test/cases/jsx-inferred-observability/fixtures/static-templates.txt @@ -0,0 +1,5 @@ +$el0; +$el1; +$el2; +static $$tmpl1 = count => _wcc`Top level count is ${count}`; +static $$tmpl2 = parity => _wcc`Parity is: ${parity}`; \ No newline at end of file diff --git a/test/cases/jsx-inferred-observability/jsx-inferred-obsevability.spec.js b/test/cases/jsx-inferred-observability/jsx-inferred-obsevability.spec.js index fc230375..b80dedb5 100644 --- a/test/cases/jsx-inferred-observability/jsx-inferred-obsevability.spec.js +++ b/test/cases/jsx-inferred-observability/jsx-inferred-obsevability.spec.js @@ -7,6 +7,7 @@ * * User Workspace * src/ + * badge.jsx * counter.jsx */ import chai from 'chai'; @@ -20,6 +21,8 @@ describe('Run WCC For ', function () { const LABEL = 'Single Custom Element using JSX and Inferred Observability'; let fixtureAttributeChangedCallback; let fixtureGetObservedAttributes; + let fixtureStaticTemplates; + let fixtureEffects; let meta; let dom; @@ -37,6 +40,11 @@ describe('Run WCC For ', function () { new URL('./fixtures/get-observed-attributes.txt', import.meta.url), 'utf-8', ); + fixtureStaticTemplates = await fs.readFile( + new URL('./fixtures/static-templates.txt', import.meta.url), + 'utf-8', + ); + fixtureEffects = await fs.readFile(new URL('./fixtures/effects.txt', import.meta.url), 'utf-8'); }); describe(LABEL, function () { @@ -55,25 +63,77 @@ describe('Run WCC For ', function () { expect(actual).to.contain(expected); }); - // + it('should infer observability by generating a static attribute method', () => { + const actual = meta['wcc-counter-jsx'].source.replace(/ /g, '').replace(/\n/g, ''); + const expected = fixtureStaticTemplates.replace(/ /g, '').replace(/\n/g, ''); + + expect(actual).to.contain(expected); + }); + + it('should infer observability by generating an effects method', () => { + const actual = meta['wcc-counter-jsx'].source.replace(/ /g, '').replace(/\n/g, ''); + const expected = fixtureEffects.replace(/ /g, '').replace(/\n/g, ''); + + expect(actual).to.contain(expected); + }); + + // it('should have the expected observability attributes on the component', () => { - const badge = dom.window.document.querySelector('wcc-badge'); + const counterDom = new JSDOM( + dom.window.document.querySelector('wcc-counter-jsx template[shadowrootmode="open"]') + .innerHTML, + ).window.document; + const badge = counterDom.querySelector('wcc-badge'); const conditionalClassSpan = badge.querySelector('span[class="unmet"]'); // conditional class rendering - expect(badge.getAttribute('data-wcc-count')).to.equal('count'); - expect(badge.getAttribute('data-wcc-ins')).to.equal('attr'); - + expect(badge.getAttribute('count')).to.equal('0'); expect(conditionalClassSpan.textContent.trim()).to.equal('0'); }); - // 0 - it('should have the expected observability attributes on the component', () => { - const span = dom.window.document.querySelector('wcc-counter-jsx span[class="red"]'); + // Top level count is {count.get()} + it('should have the expected value for the nested count signal', () => { + const counterDom = new JSDOM( + dom.window.document.querySelector('wcc-counter-jsx template[shadowrootmode="open"]') + .innerHTML, + ).window.document; + const span = counterDom.querySelector('span#one-deep'); + + expect(span.textContent.trim()).to.equal('Top level count is 0'); + expect(span.getAttribute('data-count')).to.equal('0'); + }); + + // You have clicked{' '}{count.get()}times + it('should have the expected value for the nested count signal', () => { + const counterDom = new JSDOM( + dom.window.document.querySelector('wcc-counter-jsx template[shadowrootmode="open"]') + .innerHTML, + ).window.document; + const span = counterDom.querySelector('span#two-deep span[class="red"]'); - expect(span.getAttribute('data-wcc-highlight')).to.equal('class'); - expect(span.getAttribute('data-wcc-ins')).to.equal('attr'); expect(span.textContent.trim()).to.equal('0'); }); + + // Parity is: {parity.get()} + it('should have the expected value for a signal used in another tag with the same name', () => { + const counterDom = new JSDOM( + dom.window.document.querySelector('wcc-counter-jsx template[shadowrootmode="open"]') + .innerHTML, + ).window.document; + const span = counterDom.querySelector(' span#three-deep'); + + expect(span.textContent.trim()).to.equal('Parity is: even'); + }); + + // Just some non-reactive text + it('should have the expected static content for a non-reactive element that has the same tag as another reactive tag', () => { + const counterDom = new JSDOM( + dom.window.document.querySelector('wcc-counter-jsx template[shadowrootmode="open"]') + .innerHTML, + ).window.document; + const span = counterDom.querySelector('span#non-reactive'); + + expect(span.textContent).to.equal('Just some static text'); + }); }); }); }); diff --git a/test/cases/jsx-inferred-observability/src/counter.jsx b/test/cases/jsx-inferred-observability/src/counter.jsx index 478ef0e9..625741c0 100644 --- a/test/cases/jsx-inferred-observability/src/counter.jsx +++ b/test/cases/jsx-inferred-observability/src/counter.jsx @@ -5,30 +5,34 @@ export const inferredObservability = true; export default class Counter extends HTMLElement { constructor() { super(); - this.count = 0; - this.highlight = 'red'; + this.count = new Signal.State(0); + this.highlight = new Signal.State('red'); + this.parity = new Signal.Computed(() => (this.count.get() % 2 === 0 ? 'even' : 'odd')); } increment() { - this.count += 1; - this.render(); + this.count.set(this.count.get() + 1); } decrement() { - this.count -= 1; - this.render(); + this.count.set(this.count.get() - 1); } connectedCallback() { - this.render(); + if (!this.shadowRoot) { + this.attachShadow({ + mode: 'open', + }); + this.render(); + } } render() { - const { count, highlight } = this; + const { count, highlight, parity } = this; return (
- +

Counter JSX

- + + Top level count is {count.get()} + + {/* TODO: test for nested signals */} + You have clicked{' '} - - {count} + + {count.get()} {' '} times - + Just some static text + Parity is: {parity.get()} +
); diff --git a/test/cases/tsx-inferred-observability/fixtures/attribute-changed-callback.txt b/test/cases/tsx-inferred-observability/fixtures/attribute-changed-callback.txt index bc925d60..d4ac4441 100644 --- a/test/cases/tsx-inferred-observability/fixtures/attribute-changed-callback.txt +++ b/test/cases/tsx-inferred-observability/fixtures/attribute-changed-callback.txt @@ -1,13 +1,10 @@ +static parseAttribute = value => value.charAt(0) === '{' || value.charAt(0) === '[' + ? JSON.parse(value) + : !isNaN(value) + ? parseInt(value, 10) + : value === 'true' || value === 'false' + ? value === 'true' ? true : false + : value; attributeChangedCallback(name, oldValue, newValue) { - function getValue(value) { - return value.charAt(0) === '{' || value.charAt(0) === '[' ? JSON.parse(value) : !isNaN(value) ? parseInt(value, 10) : value === 'true' || value === 'false' ? value === 'true' ? true : false : value; - } - if (newValue !== oldValue) { - switch (name) { - case 'count': - this.count = getValue(newValue); - break; - } - this.update(name,oldValue,newValue); - } + this[name].set(Counter.parseAttribute(newValue)); } \ No newline at end of file diff --git a/test/cases/tsx-inferred-observability/fixtures/effects.txt b/test/cases/tsx-inferred-observability/fixtures/effects.txt new file mode 100644 index 00000000..7842890f --- /dev/null +++ b/test/cases/tsx-inferred-observability/fixtures/effects.txt @@ -0,0 +1,11 @@ +this.$el0 = this.querySelector('div > span:nth-of-type(1)'); +this.$el1 = this.querySelector('div > span:nth-of-type(4)'); +effect(() => { + this.$el0.textContent = Counter.$$tmpl0(this.count.get()); +}); +effect(() => { + this.$el0.setAttribute('data-count', this.count.get()); +}); +effect(() => { + this.$el1.textContent = Counter.$$tmpl1(this.parity.get()); +}); \ No newline at end of file diff --git a/test/cases/tsx-inferred-observability/fixtures/static-templates.txt b/test/cases/tsx-inferred-observability/fixtures/static-templates.txt new file mode 100644 index 00000000..61e4cd2d --- /dev/null +++ b/test/cases/tsx-inferred-observability/fixtures/static-templates.txt @@ -0,0 +1,4 @@ +$el0; +$el1; +static $$tmpl0 = count => _wcc`Top level count is ${count}`; +static $$tmpl1 = parity => _wcc`Parity is: ${parity}`; \ No newline at end of file diff --git a/test/cases/tsx-inferred-observability/src/counter.tsx b/test/cases/tsx-inferred-observability/src/counter.tsx index 1aa52739..544815dc 100644 --- a/test/cases/tsx-inferred-observability/src/counter.tsx +++ b/test/cases/tsx-inferred-observability/src/counter.tsx @@ -1,21 +1,21 @@ export const inferredObservability = true; export default class Counter extends HTMLElement { - count: number; + count; + parity; constructor() { super(); - this.count = 0; + this.count = new Signal.State(0); + this.parity = new Signal.Computed(() => (this.count.get() % 2 === 0 ? 'even' : 'odd')); } increment() { - this.count += 1; - this.render(); + this.count.set(this.count.get() + 1); } decrement() { - this.count -= 1; - this.render(); + this.count.set(this.count.get() - 1); } connectedCallback() { @@ -23,7 +23,7 @@ export default class Counter extends HTMLElement { } render() { - const { count } = this; + const { count, parity } = this; return (
@@ -32,18 +32,24 @@ export default class Counter extends HTMLElement { {' '} - (function reference) - - + + Top level count is {count.get()} + + {/* TODO: test for nested signals */} + You have clicked{' '} - {count} + {count.get()} {' '} times - + Just some static text + Parity is: {parity.get()} +
); diff --git a/test/cases/tsx-inferred-observability/tsx-inferred-obsevability.spec.js b/test/cases/tsx-inferred-observability/tsx-inferred-obsevability.spec.js index 7f8dda3c..d9e98358 100644 --- a/test/cases/tsx-inferred-observability/tsx-inferred-obsevability.spec.js +++ b/test/cases/tsx-inferred-observability/tsx-inferred-obsevability.spec.js @@ -20,6 +20,8 @@ describe('Run WCC For ', function () { const LABEL = 'Single Custom Element using TSX and Inferred Observability'; let fixtureAttributeChangedCallback; let fixtureGetObservedAttributes; + let fixtureStaticTemplates; + let fixtureEffects; let meta; let dom; @@ -37,6 +39,11 @@ describe('Run WCC For ', function () { new URL('./fixtures/get-observed-attributes.txt', import.meta.url), 'utf-8', ); + fixtureStaticTemplates = await fs.readFile( + new URL('./fixtures/static-templates.txt', import.meta.url), + 'utf-8', + ); + fixtureEffects = await fs.readFile(new URL('./fixtures/effects.txt', import.meta.url), 'utf-8'); }); describe(LABEL, function () { @@ -55,14 +62,50 @@ describe('Run WCC For ', function () { expect(actual).to.contain(expected); }); - // 0 - it('should have the expected observability attributes on the component', () => { - const span = dom.window.document.querySelector('wcc-counter-tsx span[class="red"]'); + it('should infer observability by generating a static attribute method', () => { + const actual = meta['wcc-counter-tsx'].source.replace(/ /g, '').replace(/\n/g, ''); + const expected = fixtureStaticTemplates.replace(/ /g, '').replace(/\n/g, ''); + + expect(actual).to.contain(expected); + }); + + it('should infer observability by generating an effects method', () => { + const actual = meta['wcc-counter-tsx'].source.replace(/ /g, '').replace(/\n/g, ''); + const expected = fixtureEffects.replace(/ /g, '').replace(/\n/g, ''); + + expect(actual).to.contain(expected); + }); + + // Top level count is {count.get()} + it('should have the expected value for the nested count signal', () => { + const span = dom.window.document.querySelector('wcc-counter-tsx span#one-deep'); + + expect(span.textContent.trim()).to.equal('Top level count is 0'); + expect(span.getAttribute('data-count')).to.equal('0'); + }); + + // You have clicked{' '}{count.get()}times + it('should have the expected value for the nested count signal', () => { + const span = dom.window.document.querySelector( + 'wcc-counter-tsx span#two-deep span[class="red"]', + ); - expect(span.getAttribute('data-wcc-count')).to.equal('0'); - expect(span.getAttribute('data-wcc-ins')).to.equal('text'); expect(span.textContent.trim()).to.equal('0'); }); + + // Parity is: {parity.get()} + it('should have the expected value for a signal used in another tag with the same name', () => { + const span = dom.window.document.querySelector('wcc-counter-tsx span#three-deep'); + + expect(span.textContent.trim()).to.equal('Parity is: even'); + }); + + // Just some non-reactive text + it('should have the expected static content for a non-reactive element that has the same tag as another reactive tag', () => { + const span = dom.window.document.querySelector('wcc-counter-tsx span#non-reactive'); + + expect(span.textContent).to.equal('Just some static text'); + }); }); }); });