preact-missing-hooks 3.1.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +1 -0
- package/.husky/pre-push +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc +6 -0
- package/Readme.md +333 -137
- package/dist/entry.cjs +21 -0
- package/dist/entry.js +2 -0
- package/dist/entry.js.map +1 -0
- package/dist/entry.modern.mjs +2 -0
- package/dist/entry.modern.mjs.map +1 -0
- package/dist/entry.module.js +2 -0
- package/dist/entry.module.js.map +1 -0
- package/dist/entry.umd.js +2 -0
- package/dist/entry.umd.js.map +1 -0
- package/dist/index.d.ts +14 -13
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +2 -0
- package/dist/index.modern.mjs.map +1 -0
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/indexedDB/dbController.d.ts +2 -2
- package/dist/indexedDB/index.d.ts +6 -6
- package/dist/indexedDB/openDB.d.ts +1 -1
- package/dist/indexedDB/tableController.d.ts +1 -1
- package/dist/indexedDB/types.d.ts +1 -2
- package/dist/react.js +1 -0
- package/dist/react.modern.mjs +1 -0
- package/dist/react.module.js +1 -0
- package/dist/react.umd.js +1 -0
- package/dist/useEventBus.d.ts +1 -1
- package/dist/useIndexedDB.d.ts +3 -3
- package/dist/useLLMMetadata.d.ts +71 -0
- package/dist/useMutationObserver.d.ts +1 -1
- package/dist/useNetworkState.d.ts +3 -3
- package/dist/usePreferredTheme.d.ts +1 -1
- package/dist/useRageClick.d.ts +1 -1
- package/dist/useThreadedWorker.d.ts +1 -1
- package/dist/useTransition.d.ts +4 -1
- package/dist/useWorkerNotifications.d.ts +1 -1
- package/dist/useWrappedChildren.d.ts +3 -3
- package/docs/README.md +111 -0
- package/docs/index.html +58 -20
- package/docs/main.js +49 -0
- package/eslint.config.mjs +10 -0
- package/package.json +60 -6
- package/scripts/generate-entry.cjs +34 -0
- package/src/index.ts +14 -13
- package/src/indexedDB/dbController.ts +101 -92
- package/src/indexedDB/index.ts +16 -11
- package/src/indexedDB/openDB.ts +49 -49
- package/src/indexedDB/requestToPromise.ts +17 -16
- package/src/indexedDB/tableController.ts +331 -257
- package/src/indexedDB/types.ts +35 -35
- package/src/useClipboard.ts +99 -97
- package/src/useEventBus.ts +39 -36
- package/src/useIndexedDB.ts +111 -111
- package/src/useLLMMetadata.ts +418 -0
- package/src/useMutationObserver.ts +26 -26
- package/src/useNetworkState.ts +124 -122
- package/src/usePreferredTheme.ts +68 -68
- package/src/useRageClick.ts +103 -103
- package/src/useThreadedWorker.ts +165 -165
- package/src/useTransition.ts +22 -19
- package/src/useWasmCompute.ts +209 -204
- package/src/useWebRTCIP.ts +181 -176
- package/src/useWorkerNotifications.ts +28 -20
- package/src/useWrappedChildren.ts +72 -58
- package/tests/preact-as-react.ts +5 -0
- package/tests/react-adapter.tsx +12 -0
- package/tests/setup-react.ts +4 -0
- package/tests/useClipboard.test.tsx +4 -2
- package/tests/useLLMMetadata.test.tsx +149 -0
- package/tests/useThreadedWorker.test.tsx +3 -1
- package/tests/useWasmCompute.test.tsx +1 -1
- package/tests/useWebRTCIP.test.tsx +3 -1
- package/vite.config.ts +11 -4
- package/vitest.config.preact.ts +21 -0
- package/vitest.config.react.ts +36 -0
- package/vitest.workspace.ts +6 -0
|
@@ -4,6 +4,8 @@ import { useState } from 'preact/hooks'
|
|
|
4
4
|
import { render, fireEvent, waitFor } from '@testing-library/preact'
|
|
5
5
|
import { useClipboard } from '../src/useClipboard'
|
|
6
6
|
|
|
7
|
+
const isReact = !!(globalThis as unknown as { __VITEST_REACT__?: boolean }).__VITEST_REACT__
|
|
8
|
+
|
|
7
9
|
describe('useClipboard', () => {
|
|
8
10
|
const originalNavigator = global.navigator
|
|
9
11
|
|
|
@@ -15,7 +17,7 @@ describe('useClipboard', () => {
|
|
|
15
17
|
vi.useRealTimers()
|
|
16
18
|
})
|
|
17
19
|
|
|
18
|
-
it('copy succeeds and sets copied to true', async () => {
|
|
20
|
+
it.skipIf(isReact)('copy succeeds and sets copied to true', async () => {
|
|
19
21
|
vi.useFakeTimers()
|
|
20
22
|
const writeText = vi.fn().mockResolvedValue(undefined)
|
|
21
23
|
Object.defineProperty(global, 'navigator', {
|
|
@@ -108,7 +110,7 @@ describe('useClipboard', () => {
|
|
|
108
110
|
})
|
|
109
111
|
})
|
|
110
112
|
|
|
111
|
-
it('reset clears copied and error state', async () => {
|
|
113
|
+
it.skipIf(isReact)('reset clears copied and error state', async () => {
|
|
112
114
|
vi.useFakeTimers()
|
|
113
115
|
const writeText = vi.fn().mockResolvedValue(undefined)
|
|
114
116
|
Object.defineProperty(global, 'navigator', {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { h } from "preact";
|
|
3
|
+
import { render } from "@testing-library/preact";
|
|
4
|
+
import "@testing-library/jest-dom";
|
|
5
|
+
import { useLLMMetadata } from "../src/useLLMMetadata";
|
|
6
|
+
|
|
7
|
+
const SCRIPT_SELECTOR = 'script[data-llm="true"]';
|
|
8
|
+
|
|
9
|
+
function getLLMScript(): HTMLScriptElement | null {
|
|
10
|
+
return document.querySelector(SCRIPT_SELECTOR);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getLLMPayload(): unknown {
|
|
14
|
+
const script = getLLMScript();
|
|
15
|
+
if (!script?.textContent) return null;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(script.textContent);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("useLLMMetadata", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
document.head.querySelectorAll(SCRIPT_SELECTOR).forEach((el) => el.remove());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("injects script with type application/llm+json and data-llm=true", () => {
|
|
29
|
+
function TestComponent() {
|
|
30
|
+
useLLMMetadata({ route: "/", mode: "manual", title: "Home" });
|
|
31
|
+
return <div />;
|
|
32
|
+
}
|
|
33
|
+
render(<TestComponent />);
|
|
34
|
+
const script = getLLMScript();
|
|
35
|
+
expect(script).toBeInTheDocument();
|
|
36
|
+
expect(script?.getAttribute("type")).toBe("application/llm+json");
|
|
37
|
+
expect(script?.getAttribute("data-llm")).toBe("true");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("manual mode uses title, description, tags from config", () => {
|
|
41
|
+
function TestComponent() {
|
|
42
|
+
useLLMMetadata({
|
|
43
|
+
route: "/blog/ai",
|
|
44
|
+
mode: "manual",
|
|
45
|
+
title: "AI Post",
|
|
46
|
+
description: "A short desc",
|
|
47
|
+
tags: ["a", "b"],
|
|
48
|
+
});
|
|
49
|
+
return <div />;
|
|
50
|
+
}
|
|
51
|
+
render(<TestComponent />);
|
|
52
|
+
const payload = getLLMPayload() as Record<string, unknown>;
|
|
53
|
+
expect(payload).not.toBeNull();
|
|
54
|
+
expect(payload.route).toBe("/blog/ai");
|
|
55
|
+
expect(payload.title).toBe("AI Post");
|
|
56
|
+
expect(payload.description).toBe("A short desc");
|
|
57
|
+
expect(payload.tags).toEqual(["a", "b"]);
|
|
58
|
+
expect(payload.generatedAt).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("removes previous script when route changes", () => {
|
|
62
|
+
function TestComponent({ route }: { route: string }) {
|
|
63
|
+
useLLMMetadata({ route, mode: "manual", title: route });
|
|
64
|
+
return <div />;
|
|
65
|
+
}
|
|
66
|
+
const { rerender } = render(<TestComponent route="/a" />);
|
|
67
|
+
expect(getLLMScript()?.textContent).toContain('"route":"/a"');
|
|
68
|
+
rerender(<TestComponent route="/b" />);
|
|
69
|
+
const script = getLLMScript();
|
|
70
|
+
expect(script).toBeInTheDocument();
|
|
71
|
+
expect(script?.textContent).toContain('"route":"/b"');
|
|
72
|
+
expect(document.querySelectorAll(SCRIPT_SELECTOR).length).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("auto-extract mode uses document.title and builds outline from visible h1/h2", () => {
|
|
76
|
+
document.title = "Page Title";
|
|
77
|
+
const main = document.createElement("main");
|
|
78
|
+
const h1 = document.createElement("h1");
|
|
79
|
+
h1.textContent = "Intro";
|
|
80
|
+
const h2 = document.createElement("h2");
|
|
81
|
+
h2.textContent = "Section";
|
|
82
|
+
main.append(h1, h2);
|
|
83
|
+
document.body.appendChild(main);
|
|
84
|
+
|
|
85
|
+
function TestComponent() {
|
|
86
|
+
useLLMMetadata({ route: "/doc", mode: "auto-extract" });
|
|
87
|
+
return <div />;
|
|
88
|
+
}
|
|
89
|
+
render(<TestComponent />);
|
|
90
|
+
const payload = getLLMPayload() as Record<string, unknown>;
|
|
91
|
+
expect(payload?.title).toBe("Page Title");
|
|
92
|
+
expect(Array.isArray(payload?.outline)).toBe(true);
|
|
93
|
+
expect((payload?.outline as string[]).includes("Intro")).toBe(true);
|
|
94
|
+
expect((payload?.outline as string[]).includes("Section")).toBe(true);
|
|
95
|
+
main.remove();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("cleans up script on unmount", () => {
|
|
99
|
+
function Page() {
|
|
100
|
+
useLLMMetadata({ route: "/x", mode: "manual" });
|
|
101
|
+
return <div>Page</div>;
|
|
102
|
+
}
|
|
103
|
+
const { unmount } = render(<Page />);
|
|
104
|
+
expect(getLLMScript()).toBeInTheDocument();
|
|
105
|
+
unmount();
|
|
106
|
+
expect(getLLMScript()).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("default mode is manual when mode is omitted", () => {
|
|
110
|
+
function TestComponent() {
|
|
111
|
+
useLLMMetadata({ route: "/", title: "T" });
|
|
112
|
+
return <div />;
|
|
113
|
+
}
|
|
114
|
+
render(<TestComponent />);
|
|
115
|
+
const payload = getLLMPayload() as Record<string, unknown>;
|
|
116
|
+
expect(payload?.title).toBe("T");
|
|
117
|
+
expect(payload?.outline).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("accepts null config without throwing and injects minimal payload", () => {
|
|
121
|
+
function TestComponent() {
|
|
122
|
+
useLLMMetadata(null);
|
|
123
|
+
return <div />;
|
|
124
|
+
}
|
|
125
|
+
expect(() => render(<TestComponent />)).not.toThrow();
|
|
126
|
+
const payload = getLLMPayload() as Record<string, unknown>;
|
|
127
|
+
expect(payload?.route).toBe("/");
|
|
128
|
+
expect(payload?.generatedAt).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("includes canonicalUrl, ogType, siteName when provided", () => {
|
|
132
|
+
function TestComponent() {
|
|
133
|
+
useLLMMetadata({
|
|
134
|
+
route: "/page",
|
|
135
|
+
mode: "manual",
|
|
136
|
+
title: "Page",
|
|
137
|
+
canonicalUrl: "https://example.com/page",
|
|
138
|
+
ogType: "article",
|
|
139
|
+
siteName: "My Site",
|
|
140
|
+
});
|
|
141
|
+
return <div />;
|
|
142
|
+
}
|
|
143
|
+
render(<TestComponent />);
|
|
144
|
+
const payload = getLLMPayload() as Record<string, unknown>;
|
|
145
|
+
expect(payload?.canonicalUrl).toBe("https://example.com/page");
|
|
146
|
+
expect(payload?.ogType).toBe("article");
|
|
147
|
+
expect(payload?.siteName).toBe("My Site");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -4,6 +4,8 @@ import { render, waitFor } from '@testing-library/preact'
|
|
|
4
4
|
import '@testing-library/jest-dom'
|
|
5
5
|
import { useThreadedWorker } from '@/useThreadedWorker'
|
|
6
6
|
|
|
7
|
+
const isReact = !!(globalThis as unknown as { __VITEST_REACT__?: boolean }).__VITEST_REACT__
|
|
8
|
+
|
|
7
9
|
/** Worker that resolves after delay with the input value (for order/concurrency tests). */
|
|
8
10
|
function delayedWorker<T>(delayMs: number) {
|
|
9
11
|
return (data: T): Promise<T> =>
|
|
@@ -12,7 +14,7 @@ function delayedWorker<T>(delayMs: number) {
|
|
|
12
14
|
|
|
13
15
|
describe('useThreadedWorker', () => {
|
|
14
16
|
describe('sequential mode', () => {
|
|
15
|
-
it('runs one task at a time and returns result', async () => {
|
|
17
|
+
it.skipIf(isReact)('runs one task at a time and returns result', async () => {
|
|
16
18
|
const worker = vi.fn().mockResolvedValue('done')
|
|
17
19
|
let api: ReturnType<typeof useThreadedWorker<string, string>>
|
|
18
20
|
|
|
@@ -19,7 +19,7 @@ describe('useWasmCompute', () => {
|
|
|
19
19
|
URL.revokeObjectURL = originalRevokeObjectURL;
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
it('returns error when window is undefined (SSR)', async () => {
|
|
22
|
+
it.skipIf(!!(globalThis as unknown as { __VITEST_REACT__?: boolean }).__VITEST_REACT__)('returns error when window is undefined (SSR)', async () => {
|
|
23
23
|
const container = document.createElement('div');
|
|
24
24
|
document.body.appendChild(container);
|
|
25
25
|
(global as unknown as { window: undefined }).window = undefined;
|
|
@@ -4,6 +4,8 @@ import { render, waitFor } from '@testing-library/preact';
|
|
|
4
4
|
import '@testing-library/jest-dom';
|
|
5
5
|
import { useWebRTCIP } from '@/useWebRTCIP';
|
|
6
6
|
|
|
7
|
+
const isReact = !!(globalThis as unknown as { __VITEST_REACT__?: boolean }).__VITEST_REACT__;
|
|
8
|
+
|
|
7
9
|
describe('useWebRTCIP', () => {
|
|
8
10
|
const originalRTCPeerConnection = global.RTCPeerConnection;
|
|
9
11
|
|
|
@@ -37,7 +39,7 @@ describe('useWebRTCIP', () => {
|
|
|
37
39
|
expect(getByTestId('ips').textContent).toBe('');
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
it('starts with loading true and then resolves with mock ICE candidate', async () => {
|
|
42
|
+
it.skipIf(isReact)('starts with loading true and then resolves with mock ICE candidate', async () => {
|
|
41
43
|
vi.useFakeTimers();
|
|
42
44
|
|
|
43
45
|
let onIceCandidate: (e: { candidate: { candidate: string } | null }) => void = () => { };
|
package/vite.config.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import { defineConfig } from
|
|
2
|
-
import path from
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
5
|
resolve: {
|
|
6
6
|
alias: {
|
|
7
|
-
|
|
7
|
+
"@": path.resolve(__dirname, "./src"),
|
|
8
8
|
},
|
|
9
9
|
},
|
|
10
10
|
test: {
|
|
11
11
|
globals: true,
|
|
12
|
-
environment:
|
|
12
|
+
environment: "jsdom",
|
|
13
13
|
setupFiles: [],
|
|
14
|
+
include: ["tests/**/*.test.{ts,tsx}"],
|
|
15
|
+
poolOptions: {
|
|
16
|
+
threads: {
|
|
17
|
+
maxThreads: 8,
|
|
18
|
+
minThreads: 1,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
14
21
|
},
|
|
15
22
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: {
|
|
7
|
+
"@": path.resolve(__dirname, "./src"),
|
|
8
|
+
react: path.resolve(__dirname, "./tests/preact-as-react.ts"),
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
test: {
|
|
12
|
+
name: "preact",
|
|
13
|
+
globals: true,
|
|
14
|
+
environment: "jsdom",
|
|
15
|
+
setupFiles: [],
|
|
16
|
+
include: ["tests/**/*.test.{ts,tsx}"],
|
|
17
|
+
poolOptions: {
|
|
18
|
+
threads: { maxThreads: 8, minThreads: 1 },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const rootDir = __dirname;
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: [
|
|
9
|
+
{
|
|
10
|
+
find: "preact/hooks",
|
|
11
|
+
replacement: path.resolve(rootDir, "node_modules/react"),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
find: "preact",
|
|
15
|
+
replacement: path.resolve(rootDir, "tests/react-adapter.tsx"),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
find: "@testing-library/preact",
|
|
19
|
+
replacement: path.resolve(rootDir, "node_modules/@testing-library/react"),
|
|
20
|
+
},
|
|
21
|
+
{ find: "@", replacement: path.resolve(rootDir, "./src") },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
test: {
|
|
25
|
+
name: "react",
|
|
26
|
+
globals: true,
|
|
27
|
+
environment: "jsdom",
|
|
28
|
+
setupFiles: [path.resolve(rootDir, "tests/setup-react.ts")],
|
|
29
|
+
include: ["tests/**/*.test.{ts,tsx}"],
|
|
30
|
+
testTimeout: 10000,
|
|
31
|
+
hookTimeout: 10000,
|
|
32
|
+
poolOptions: {
|
|
33
|
+
threads: { maxThreads: 8, minThreads: 1 },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|