11
', $result->html); + $state = json_decode($result->payload, true, flags: JSON_THROW_ON_ERROR); + $this->assertSame(['value' => 10], $state['props']['data']); + $this->assertSame(1, $state['protocol']); + $this->assertSame('19.3.1', $state['reactVersion']); + $this->assertStringNotContainsString(__DIR__, $result->payload); + $this->assertStringNotContainsString('toJSON', $result->payload); + } + + public function testDataShapesAndCallableLookingArraysRemainData(): void + { + $data = (object) [ + 'emptyObject' => new \stdClass(), + 'emptyArray' => [], + 'null' => null, + 'callable' => ['DateTime', 'createFromFormat'], + '__proto__' => (object) ['polluted' => true], + 'constructor' => ['prototype' => ['safe' => true]], + 'toJSON' => 'ordinary data', + 'numericMap' => [2 => 'two', 0 => 'zero'], + ]; + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['data' => $data], + export: 'Shapes', + options: $this->options(), + ); + $state = json_decode($result->payload, flags: JSON_THROW_ON_ERROR); + $this->assertInstanceOf(\stdClass::class, $state->props->data->emptyObject); + $this->assertSame([], $state->props->data->emptyArray); + $this->assertSame(['DateTime', 'createFromFormat'], $state->props->data->callable); + $this->assertTrue($state->props->data->__proto__->polluted); + $this->assertStringContainsString('"toJSON":"ordinary data"', $result->html); + $this->assertFalse($this->engine->eval('Object.hasOwn(Object.prototype, "polluted")')); + } + + public function testEmptyRootPropsAreObjectsAndClassDefaultsAndReservedPropsMatchCreateElement(): void + { + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['key' => 'root-key', '__self' => 'hidden', '__source' => 'hidden', 'ref' => null], + export: 'Defaults', + options: $this->options(), + ); + $this->assertSame('fallback:label
', $result->html); + $empty = $this->engine->renderForHydration(__DIR__ . '/hydration/Root.jsx', export: 'Defaults', options: $this->options()); + $this->assertInstanceOf(\stdClass::class, json_decode($empty->payload)->props); + $this->assertSame('fallback:label
', $empty->html); + } + + public function testFiniteDoublesSignedZeroAndSafeIntegersRoundTripDespiteHostPrecision(): void + { + $previous = ini_set('serialize_precision', '3'); + try { + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['data' => ['zero' => -0.0, 'decimal' => 1.2345678901234567, 'max' => 9007199254740991, 'large' => 1.0e100]], + export: 'Shapes', + options: $this->options(), + ); + $this->assertSame('3', ini_get('serialize_precision')); + $state = json_decode($result->payload); + $this->assertSame(-INF, fdiv(1.0, $state->props->data->zero)); + $this->assertSame(1.2345678901234567, $state->props->data->decimal); + $this->assertSame(9007199254740991, $state->props->data->max); + $this->assertStringContainsString('negative-zero', $result->html); + $this->assertStringContainsString('1.2345678901234567', $result->html); + } finally { + ini_set('serialize_precision', $previous); + } + } + + public function testScriptSafetyAndAttributeEscapingAreSeparate(): void + { + $text = '' . "\u{2028}\u{2029}"; + $id = 'root"<&\''; + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['data' => ['text' => $text]], + export: 'Shapes', + options: $this->options($id), + ); + $this->assertStringNotContainsString('<', $result->payload); + $this->assertStringNotContainsString('"', $result->payload); + $this->assertSame($text, json_decode($result->payload)->props->data->text); + $this->assertStringContainsString('id="root"<&'"', $result->toHtml()); + $this->assertSame(1, substr_count($result->toHtml(), '' . "\u{2028}\u{2029}", + ]]], +]; +$html = ''; +foreach ($cases as $case) { + $engine = new Engine(); + try { + $rendered = $engine->renderForHydration( + $consumer . '/components/Root.jsx', + $case['props'], + export: $case['export'], + options: new HydrationOptions( + rootId: $case['id'], + entryId: 'contract', + buildId: 'build-123', + identifierPrefix: $case['id'] . '-', + ), + ); + $html .= $rendered->toHtml(); + } finally { + $engine->close(); + } +} +echo $html; diff --git a/tests/Jsx/hydration/Root.jsx b/tests/Jsx/hydration/Root.jsx new file mode 100644 index 00000000..87888169 --- /dev/null +++ b/tests/Jsx/hydration/Root.jsx @@ -0,0 +1,58 @@ +import React, { Component, createContext, useContext, useId, useState } from 'react'; +import { format } from './format.js'; + +const Data = createContext(null); + +function Counter({ count, increment }) { + const data = useContext(Data); + const id = useId(); + return ; +} + +function Value({ count }) { + const data = useContext(Data); + return ; +} + +export default function Root({ data }) { + const [count, setCount] = useState(0); + // Deliberate impurity proves that the payload is the pre-render capture. + if (data.mutate) data.value += 1; + return +{this.props.label}:{Object.keys(this.props).sort().join(',')}
; + } +} + +export function Shapes({ data }) { + return{JSON.stringify(data)}:{Object.is(data.zero, -0) ? 'negative-zero' : 'positive-zero'}:{data.decimal};
+}
+
+export function Keys(props) {
+ let brand;
+ try {
+ Number.prototype.valueOf.call(props.data);
+ brand = 'number';
+ } catch (error) {
+ brand = error.name;
+ }
+ return {Object.keys(props).sort().join(',')}:{Reflect.ownKeys(props.data).join(',')}:{JSON.stringify(props.data)}
+ :{String(props.data)}:{Object.prototype.toString.call(props.data)}:{brand};
+}
+
+export function Document() {
+ return {data.value}
; +} diff --git a/tests/Jsx/hydration/format.js b/tests/Jsx/hydration/format.js new file mode 100644 index 00000000..9141334f --- /dev/null +++ b/tests/Jsx/hydration/format.js @@ -0,0 +1,3 @@ +export function format(value) { + return `Count: ${value}`; +} diff --git a/tests/Jsx/oracle/hydration-contract.mjs b/tests/Jsx/oracle/hydration-contract.mjs new file mode 100644 index 00000000..e5170269 --- /dev/null +++ b/tests/Jsx/oracle/hydration-contract.mjs @@ -0,0 +1,214 @@ +// Qualify the actual Composer archive in an isolated downstream consumer. +// Node/esbuild are test/build tools; PHP alone executes the server render. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import esbuild from 'esbuild'; +import { JSDOM } from 'jsdom'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repo = path.resolve(here, '../../..'); +const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'phasis-hydration-')); +const sourceMode = process.argv.includes('--source'); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const until = async (test) => { + const deadline = Date.now() + 5000; + while (!test()) { + if (Date.now() >= deadline) throw new Error('Timed out waiting for React.'); + await sleep(10); + } +}; +let checks = 0; +try { + if (sourceMode) { + fs.mkdirSync(path.join(temp, 'vendor/phasis'), { recursive: true }); + fs.symlinkSync(repo, path.join(temp, 'vendor/phasis/phasis'), 'dir'); + fs.writeFileSync(path.join(temp, 'vendor/autoload.php'), ` /react\/cjs\/react\.(production|development)\.js$/.test(input)); + assert.equal(reactInputs.length, 1, 'The helper and components must share one React copy.'); + const code = bundle.outputFiles[0].text; + const dom = new JSDOM(`${html}`, { + runScripts: 'outside-only', pretendToBeVisual: true, url: 'https://consumer.test/', + }); + const { window } = dom; + const logs = []; + window.console.error = (...args) => logs.push(args.map(String).join(' ')); + window.console.warn = (...args) => logs.push(args.map(String).join(' ')); + const roots = []; + try { + window.eval(code); + const { hydrate, HydrationError, components } = window.contract; + const errors = []; + const options = (name = 'default') => ({ + entryId: 'contract', buildId: 'build-123', export: name, + onRecoverableError: (error) => errors.push(error), + onUncaughtError: (error) => errors.push(error), + onCaughtError: (error) => errors.push(error), + }); + const before = Array.from(window.document.querySelectorAll('div[id]')).flatMap((root) => { + const nodes = []; + const walker = window.document.createTreeWalker(root, 1 | 4 | 128); + for (let node = walker.nextNode(); node; node = walker.nextNode()) nodes.push(node); + return nodes; + }); + for (const [id, name] of [['alpha', 'default'], ['beta', 'default'], ['named', 'Defaults'], ['empty', 'Defaults'], + ['shapes', 'Shapes'], ['keys', 'Keys']]) { + roots.push(hydrate(window.document.getElementById(id), components, options(name))); + } + await sleep(100); + assert.deepEqual(errors, []); + assert.deepEqual(logs, [], 'Hydration must produce no React warnings.'); + assert.ok(before.every((node) => node.isConnected), 'All server elements, text and comments must be reused.'); + assert.equal(window.document.querySelector('#alpha output').textContent, '11:0'); + const alphaState = JSON.parse(window.document.getElementById('alpha').nextElementSibling.textContent); + assert.equal(alphaState.props.data.value, 10, 'Payload must precede render mutation.'); + assert.equal(window.document.querySelector('#named p').textContent, 'fallback:label'); + assert.equal(window.document.querySelector('#empty p').textContent, 'fallback:label'); + const a = window.document.querySelector('#alpha button'); + const b = window.document.querySelector('#beta button'); + assert.ok(a.id.startsWith('_alpha-')); + assert.ok(b.id.startsWith('_beta-')); + assert.notEqual(a.id, b.id); + assert.equal(window.exploit, undefined); + assert.equal(window.document.querySelectorAll('script').length, 6, 'Captured text must not escape an inert state tag.'); + assert.equal(window.Object.prototype.polluted, undefined); + a.click(); + await until(() => window.document.querySelector('#alpha output').textContent.endsWith(':1')); + assert.equal(window.document.querySelector('#beta output').textContent, '20:0'); + checks += 15; + + // Mismatched metadata fails synchronously, preserving the exact nodes. + const base = window.document.getElementById('beta'); + const baseState = JSON.parse(base.nextElementSibling.textContent); + const make = (change = () => {}) => { + const root = window.document.createElement('div'); + const id = `failure-${checks}`; + root.id = id; + root.innerHTML = base.innerHTML; + const tag = window.document.createElement('script'); + tag.type = 'application/json'; + tag.setAttribute('data-phasis-state', id); + const state = { ...baseState, rootId: id }; + change(state); + tag.textContent = JSON.stringify(state); + window.document.body.append(root, tag); + return { root, tag }; + }; + const cases = [ + ['protocol', (state) => { state.protocol = 2; }], + ['entry', (state) => { state.entryId = 'other'; }], + ['build', (state) => { state.buildId = 'old'; }], + ['export', (state) => { state.export = 'Defaults'; }], + ['React', (state) => { state.reactVersion = '0.0.0'; }], + ['root', (state) => { state.rootId = 'other'; }], + ['extra executable URL', (state) => { state.url = 'https://untrusted.test/run.js'; }], + ['array props', (state) => { state.props = []; }], + ['null props', (state) => { state.props = null; }], + ['missing key', (state) => { delete state.identifierPrefix; }], + ]; + for (const [name, change] of cases) { + const { root, tag } = make(change); + const node = root.firstChild; + const original = root.innerHTML; + const reports = []; + assert.throws(() => hydrate(root, components, { ...options(), onError: (error) => reports.push(error) }), HydrationError, name); + assert.equal(reports.length, 1); + assert.equal(root.innerHTML, original); + assert.equal(root.firstChild, node); + root.remove(); tag.remove(); + checks++; + } + for (const text of ['{', '{"protocol":1}', base.nextElementSibling.textContent.replace('20', '1e400')]) { + const { root, tag } = make(); + tag.textContent = text; + assert.throws(() => hydrate(root, components, options()), HydrationError); + root.remove(); tag.remove(); + checks++; + } + const inherited = make(); + assert.throws(() => hydrate(inherited.root, Object.create({ default: components.default }), options()), HydrationError); + inherited.root.remove(); inherited.tag.remove(); + checks++; + assert.throws(() => hydrate(base, components, options()), HydrationError, 'duplicate initialization'); + window.eval(code); + assert.throws(() => window.contract.hydrate(base, components, options()), /already been initialized/, 'duplicate helper bundle'); + checks += 2; + + // Real markup mismatches go to the caller's React diagnostics. + const mismatch = make(); + mismatch.root.querySelector('output').textContent = 'stale'; + roots.push(hydrate(mismatch.root, components, options())); + await until(() => errors.length > 0); + assert.ok(errors.some((error) => /hydrat|match|#418/i.test(String(error)))); + checks++; + } finally { + for (const root of roots) root.unmount(); + window.close(); + } + console.log(`hydration contract: ${mode} passed`); + } + console.log(`hydration contract: ${checks} checks passed (${sourceMode ? 'source checkout' : 'Composer archive consumer'})`); +} finally { + fs.rmSync(temp, { recursive: true, force: true }); +} diff --git a/tests/Jsx/oracle/package-lock.json b/tests/Jsx/oracle/package-lock.json index 890a6c3d..4d517a6a 100644 --- a/tests/Jsx/oracle/package-lock.json +++ b/tests/Jsx/oracle/package-lock.json @@ -28,6 +28,11 @@ "react-markdown": "10.1.0", "tailwind-merge": "3.7.0", "use-sync-external-store": "1.7.0" + }, + "devDependencies": { + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "typescript": "7.0.2" } }, "node_modules/@asamuzakjp/css-color": { @@ -631,17 +636,366 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", @@ -771,8 +1125,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/data-urls": { "version": "5.0.0", @@ -2198,6 +2551,41 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", diff --git a/tests/Jsx/oracle/package.json b/tests/Jsx/oracle/package.json index 190ed39b..558603c3 100644 --- a/tests/Jsx/oracle/package.json +++ b/tests/Jsx/oracle/package.json @@ -17,6 +17,7 @@ "lucide-react": "1.56.0", "preact": "11.0.1", "preact-render-to-string": "6.8.1", + "prettier": "3.9.10", "prop-types": "15.8.1", "react": "19.3.1", "react-dom": "19.3.1", @@ -24,7 +25,11 @@ "react-is": "19.3.1", "react-markdown": "10.1.0", "tailwind-merge": "3.7.0", - "use-sync-external-store": "1.7.0", - "prettier": "3.9.10" + "use-sync-external-store": "1.7.0" + }, + "devDependencies": { + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "typescript": "7.0.2" } }