@pwa-platform/react 0.1.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PWA Platform contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # @pwa-platform/react
2
+
3
+ React 19 binding for PWA Platform.
4
+
5
+ Use `PwaProvider` and `usePwa()` in a React 19 application. Also configure `@pwa-platform/vite` for build artifacts.
6
+
7
+ This is a `0.1.0` prerelease. It provides online use, installation integration, static precaching, a safe offline fallback, and controlled updates. It does not enable runtime business API caching, private data caching, automatic write replay, or Push for applications. Production deployment requires the application and infrastructure checks described in the PWA Platform release runbook.
8
+
9
+ License: MIT.
@@ -0,0 +1,28 @@
1
+ import type { PwaClient, PwaClientConfig } from "@pwa-platform/client-runtime";
2
+ import { type ReactElement, type ReactNode } from "react";
3
+ import { type PwaMethods, type PwaState } from "./store.js";
4
+ export type { PwaMethods, PwaState } from "./store.js";
5
+ export type PwaBinding = PwaMethods & {
6
+ readonly state: PwaState;
7
+ };
8
+ export type PwaProviderProps = {
9
+ readonly config: PwaClientConfig;
10
+ /**
11
+ * Defaults to a facade built from `config`; passed explicitly only to inject a fake in tests. Whatever is passed
12
+ * is disposed when the provider unmounts — the binding takes ownership of it for its lifetime.
13
+ */
14
+ readonly client?: PwaClient;
15
+ /**
16
+ * Automatic periodic checks, forwarded to `createPwaClient` when the provider builds its own facade. Omitted
17
+ * means off. Meaningless together with `client` — an injected facade is already built, so `PwaProvider` throws
18
+ * during render when both are passed (spec: 修订:主动检查更新).
19
+ */
20
+ readonly updateCheck?: {
21
+ readonly intervalMs: number;
22
+ };
23
+ readonly children?: ReactNode;
24
+ };
25
+ export declare function PwaProvider(props: PwaProviderProps): ReactElement;
26
+ /** Reads the binding provided by `PwaProvider`. Throws when no provider is above it in the tree. */
27
+ export declare function usePwa(): PwaBinding;
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/E,OAAO,EAQL,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAOL,KAAK,UAAU,EACf,KAAK,QAAQ,EAEd,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEnE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE;QAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;CAC/B,CAAC;AAMF,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,YAAY,CA2BjE;AAED,oGAAoG;AACpG,wBAAgB,MAAM,IAAI,UAAU,CAwBnC"}
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ import { createContext, createElement, useContext, useEffect, useMemo, useState, useSyncExternalStore, } from "react";
2
+ import { bindFacade, CLIENT_AND_UPDATE_CHECK_ERROR, createStore, INITIAL_STATE, providerEffectDeps, SERVER_METHODS, } from "./store.js";
3
+ const StoreContext = createContext(null);
4
+ // No JSX anywhere in this package: `tsconfig.base.json` sets no `jsx` option, and changing the whole repository's
5
+ // compiler configuration for one provider that renders no elements of its own is not worth it.
6
+ export function PwaProvider(props) {
7
+ const { config, client, updateCheck, children } = props;
8
+ // Checked first, before any hook runs: an injected facade is already built, and the provider has no way to
9
+ // configure its automatic update check after the fact. Because this precedes every hook call, it can also be
10
+ // exercised directly as a plain function call in a unit test, without a renderer.
11
+ if (client !== undefined && updateCheck !== undefined) {
12
+ throw new Error(CLIENT_AND_UPDATE_CHECK_ERROR);
13
+ }
14
+ // One store per provider instance, created lazily so it already exists on the first render and children can read
15
+ // state immediately. The facade is attached by the effect below, not here.
16
+ const [store] = useState(createStore);
17
+ useEffect(
18
+ // The whole body lives in `bindFacade` so it can be tested without a renderer. Creating the facade inside the
19
+ // effect is also what StrictMode requires: it deliberately mounts, unmounts and mounts again, and a facade
20
+ // built during render would be disposed by the first cleanup and then reused dead on the second mount.
21
+ () => bindFacade(store, config, client, updateCheck),
22
+ // Depends on the config's *fields*, not on the object, and on `updateCheck`'s `intervalMs` field rather than
23
+ // the object itself — for the same reason. `<PwaProvider config={{ ... }} updateCheck={{ ... }}>` builds new
24
+ // objects on every render, and an object dependency would tear the facade down and build a fresh, unregistered
25
+ // one each time — with the store's flags still describing the old one. React's own docs flag object
26
+ // dependencies for exactly this reason. See `providerEffectDeps` for the list itself, extracted for a unit
27
+ // test this component cannot carry (spec: 已知限制).
28
+ providerEffectDeps(store, client, config, updateCheck));
29
+ return createElement(StoreContext.Provider, { value: store }, children);
30
+ }
31
+ /** Reads the binding provided by `PwaProvider`. Throws when no provider is above it in the tree. */
32
+ export function usePwa() {
33
+ const store = useContext(StoreContext);
34
+ if (store === null) {
35
+ throw new Error("No PWA binding found: wrap the tree in a PwaProvider first");
36
+ }
37
+ // The third argument is what React reads on the server and while hydrating. Without it a server render throws
38
+ // (ADR-0016, 2026-09-17 amendment). The provider's effect never runs on the server, so no facade exists there and
39
+ // the initial state is the only true answer.
40
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot);
41
+ // On the server the store's methods would wait for a facade that is never attached; hand out ones that reject.
42
+ const methods = typeof window === "undefined" ? SERVER_METHODS : store;
43
+ // Memoised so the binding keeps its identity between renders. Without this, an application that lists the
44
+ // binding in a dependency array would re-run that effect on every single render.
45
+ return useMemo(() => ({
46
+ state,
47
+ register: methods.register,
48
+ promptInstall: methods.promptInstall,
49
+ applyUpdate: methods.applyUpdate,
50
+ logout: methods.logout,
51
+ checkForUpdate: methods.checkForUpdate,
52
+ }), [state, methods]);
53
+ }
54
+ /** Module-level, so React sees the same function on every render. */
55
+ function getServerSnapshot() {
56
+ return INITIAL_STATE;
57
+ }
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,aAAa,EACb,aAAa,EACb,UAAU,EACV,SAAS,EACT,OAAO,EACP,QAAQ,EACR,oBAAoB,GAGrB,MAAM,OAAO,CAAC;AACf,OAAO,EACL,UAAU,EACV,6BAA6B,EAC7B,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,cAAc,GAIf,MAAM,YAAY,CAAC;AAsBpB,MAAM,YAAY,GAAG,aAAa,CAAkB,IAAI,CAAC,CAAC;AAE1D,kHAAkH;AAClH,+FAA+F;AAC/F,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IACxD,2GAA2G;IAC3G,6GAA6G;IAC7G,kFAAkF;IAClF,IAAI,MAAM,KAAK,SAAS,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;IACD,iHAAiH;IACjH,2EAA2E;IAC3E,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IAEtC,SAAS;IACP,8GAA8G;IAC9G,2GAA2G;IAC3G,uGAAuG;IACvG,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC;IACpD,6GAA6G;IAC7G,6GAA6G;IAC7G,+GAA+G;IAC/G,oGAAoG;IACpG,2GAA2G;IAC3G,iDAAiD;IACjD,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,CACvD,CAAC;IAEF,OAAO,aAAa,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,QAAQ,CAAC,CAAC;AAC1E,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,MAAM;IACpB,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,8GAA8G;IAC9G,kHAAkH;IAClH,6CAA6C;IAC7C,MAAM,KAAK,GAAG,oBAAoB,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;IAC1F,+GAA+G;IAC/G,MAAM,OAAO,GAAe,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC;IACnF,0GAA0G;IAC1G,iFAAiF;IACjF,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC;QACL,KAAK;QACL,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,cAAc,EAAE,OAAO,CAAC,cAAc;KACvC,CAAC,EACF,CAAC,KAAK,EAAE,OAAO,CAAC,CACjB,CAAC;AACJ,CAAC;AAED,qEAAqE;AACrE,SAAS,iBAAiB;IACxB,OAAO,aAAa,CAAC;AACvB,CAAC"}
@@ -0,0 +1,88 @@
1
+ import { type PwaClient, type PwaClientConfig, type PwaClientEvent } from "@pwa-platform/client-runtime";
2
+ export type PwaState = {
3
+ /** `registered` was emitted: the worker is registered at the plan's scope. */
4
+ readonly registered: boolean;
5
+ /** `install-eligible` was emitted and the app is not installed yet: `promptInstall()` can be called. */
6
+ readonly installEligible: boolean;
7
+ readonly installed: boolean;
8
+ /** `update-waiting` was emitted: a new version is waiting and `applyUpdate()` can be called. */
9
+ readonly updateWaiting: boolean;
10
+ };
11
+ export declare const INITIAL_STATE: PwaState;
12
+ /**
13
+ * Applies one lifecycle event and returns the next state — the *same* object when nothing changes. That identity
14
+ * is load-bearing: `useSyncExternalStore` compares snapshots by reference and would re-render forever if a fresh
15
+ * object came back every time.
16
+ *
17
+ * `installed` and `update-applied` clear the flags for prompts the facade no longer offers: `appinstalled` drops
18
+ * the saved install prompt, and a controller change completes a previously announced update prompt. The other
19
+ * events never fall back to false, because the facade emits no cancellation event for them. `logout()` is the visible case: it returns a boolean
20
+ * without emitting anything, so `registered` stays true (spec: known limitations).
21
+ */
22
+ export declare function reduce(state: PwaState, event: PwaClientEvent): PwaState;
23
+ /** The five facade methods, taken from `PwaClient` so the signatures cannot drift from it. */
24
+ export type PwaMethods = Pick<PwaClient, "register" | "promptInstall" | "applyUpdate" | "logout" | "checkForUpdate">;
25
+ /**
26
+ * The message every method rejects with during server-side rendering. The Vue package carries the same text, and
27
+ * the server-side parity suite in examples-browser-e2e compares the two.
28
+ */
29
+ export declare const SERVER_RENDERING_ERROR = "PWA methods are unavailable during server-side rendering: call them in the browser after hydration";
30
+ /**
31
+ * What the binding hands out on the server (ADR-0016, 2026-09-17 amendment). There is no service worker, no install
32
+ * prompt and no facade there, so every method rejects at once — waiting for a facade that will never be attached
33
+ * would leave the call pending forever, and touching a browser API would throw something far less clear.
34
+ */
35
+ export declare const SERVER_METHODS: PwaMethods;
36
+ /**
37
+ * The message `PwaProvider` throws when both `client` and `updateCheck` are passed. The Vue package carries the
38
+ * same text (spec: 修订:主动检查更新, 框架绑定增量), and each package's test suite asserts it word for word.
39
+ */
40
+ export declare const CLIENT_AND_UPDATE_CHECK_ERROR = "Cannot pass both client and updateCheck: an injected client is already built, so the binding has no way to configure its automatic update check";
41
+ export type PwaStore = PwaMethods & {
42
+ /** `useSyncExternalStore`'s subscribe: the callback runs only when the snapshot actually changed. */
43
+ subscribe(onChange: () => void): () => void;
44
+ getSnapshot(): PwaState;
45
+ /**
46
+ * Binds a facade and starts tracking its events. Called from the provider's effect rather than during render,
47
+ * which is what StrictMode's deliberate mount → unmount → mount requires: each mount attaches a fresh facade
48
+ * and each cleanup detaches it, so a disposed facade is never reused.
49
+ */
50
+ attach(client: PwaClient): void;
51
+ /**
52
+ * Stops tracking and resets the state. The reset is the point: with no facade attached, the flags describe
53
+ * nothing. Leaving them set is what let a rebuilt provider show "registered" for a facade that had never
54
+ * registered. The facade itself is disposed by whoever created it.
55
+ */
56
+ detach(): void;
57
+ };
58
+ export declare function createStore(): PwaStore;
59
+ /** The shape of `PwaProviderProps.updateCheck`, repeated here rather than imported to keep this file React-free. */
60
+ type UpdateCheckOption = {
61
+ readonly intervalMs: number;
62
+ };
63
+ /**
64
+ * Creates (or takes) a facade, attaches it to the store, and returns the teardown. This is the entire body of the
65
+ * provider's effect, extracted so it can be unit tested without a renderer: the provider is then a one-liner with
66
+ * nowhere to hide a defect.
67
+ *
68
+ * `client` and `updateCheck` are positional optional parameters rather than fields on an options object on
69
+ * purpose. The caller holds them as `X | undefined` (they come from props), and under `exactOptionalPropertyTypes`
70
+ * an optional *property* rejects an explicitly passed `undefined`. Optional parameters carry no such restriction,
71
+ * so this avoids a `client?: PwaClient | undefined` signature that reads like a mistake.
72
+ *
73
+ * The returned teardown disposes the facade even when it was injected — the binding takes ownership for its
74
+ * lifetime, which keeps the Vue and React sides behaving alike. `PwaProvider` is responsible for rejecting a call
75
+ * that passes both `client` and `updateCheck` before this function is ever reached.
76
+ */
77
+ export declare function bindFacade(store: PwaStore, config: PwaClientConfig, client?: PwaClient, updateCheck?: UpdateCheckOption): () => void;
78
+ /**
79
+ * The provider's `useEffect` dependency list, extracted for the same reason as `bindFacade`: this package ships no
80
+ * renderer, so nothing inside the component itself has a unit test. What matters here is the *shape* of the list
81
+ * React compares with `Object.is` — in particular that `updateCheck` contributes its `intervalMs` field and not
82
+ * the option object itself. An application that passes a fresh `{ intervalMs }` literal on every render must not
83
+ * tear the facade down and rebuild it just because that object's identity changed, exactly as `config`'s fields
84
+ * are read individually rather than depending on the `config` object.
85
+ */
86
+ export declare function providerEffectDeps(store: PwaStore, client: PwaClient | undefined, config: PwaClientConfig, updateCheck: UpdateCheckOption | undefined): readonly unknown[];
87
+ export {};
88
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAYA,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE1H,MAAM,MAAM,QAAQ,GAAG;IACrB,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,wGAAwG;IACxG,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC,CAAC;AAIF,eAAO,MAAM,aAAa,EAAE,QAK1B,CAAC;AAEH;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,cAAc,GAAG,QAAQ,CAqBvE;AAED,8FAA8F;AAC9F,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,eAAe,GAAG,aAAa,GAAG,QAAQ,GAAG,gBAAgB,CAAC,CAAC;AAErH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,uGAAuG,CAAC;AAM3I;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,UAM3B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,6BAA6B,oJACyG,CAAC;AAEpJ,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG;IAClC,qGAAqG;IACrG,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,WAAW,IAAI,QAAQ,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,wBAAgB,WAAW,IAAI,QAAQ,CAiFtC;AAED,oHAAoH;AACpH,KAAK,iBAAiB,GAAG;IAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,SAAS,EAClB,WAAW,CAAC,EAAE,iBAAiB,GAC9B,MAAM,IAAI,CAYZ;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,MAAM,EAAE,eAAe,EACvB,WAAW,EAAE,iBAAiB,GAAG,SAAS,GACzC,SAAS,OAAO,EAAE,CAWpB"}
package/dist/store.js ADDED
@@ -0,0 +1,203 @@
1
+ // The whole binding, in plain TypeScript with no React in sight: the state machine, the subscription, the five
2
+ // forwarded methods, and the facade's lifetime. React's side is a provider that calls `bindFacade` from an effect
3
+ // and a hook that reads a snapshot.
4
+ //
5
+ // The split is deliberate. `useSyncExternalStore` only runs inside a render, and this package ships no renderer
6
+ // (spec: known limitations), so anything left in the component would have no unit test at all — and a defect did
7
+ // hide there: the provider used to rebuild the facade whenever the `config` prop changed identity while the store
8
+ // kept its old flags, so the UI read "registered" against a facade that had never registered. `bindFacade` exists
9
+ // so that whole create → attach → detach → dispose cycle is testable without mounting anything.
10
+ //
11
+ // This is the React package's own copy of the state machine; the Vue package has its own and neither imports the
12
+ // other. The parity suite compares their state sequences.
13
+ import { createPwaClient } from "@pwa-platform/client-runtime";
14
+ // Frozen, not merely `readonly`: it is a shared module-level object and every store starts from it, so a stray
15
+ // write would change the starting state of every store created afterwards.
16
+ export const INITIAL_STATE = Object.freeze({
17
+ registered: false,
18
+ installEligible: false,
19
+ installed: false,
20
+ updateWaiting: false,
21
+ });
22
+ /**
23
+ * Applies one lifecycle event and returns the next state — the *same* object when nothing changes. That identity
24
+ * is load-bearing: `useSyncExternalStore` compares snapshots by reference and would re-render forever if a fresh
25
+ * object came back every time.
26
+ *
27
+ * `installed` and `update-applied` clear the flags for prompts the facade no longer offers: `appinstalled` drops
28
+ * the saved install prompt, and a controller change completes a previously announced update prompt. The other
29
+ * events never fall back to false, because the facade emits no cancellation event for them. `logout()` is the visible case: it returns a boolean
30
+ * without emitting anything, so `registered` stays true (spec: known limitations).
31
+ */
32
+ export function reduce(state, event) {
33
+ switch (event.type) {
34
+ case "registered":
35
+ return state.registered ? state : { ...state, registered: true };
36
+ case "install-eligible":
37
+ return state.installEligible ? state : { ...state, installEligible: true };
38
+ case "installed":
39
+ return state.installed && !state.installEligible
40
+ ? state
41
+ : { ...state, installed: true, installEligible: false };
42
+ case "update-waiting":
43
+ return state.updateWaiting ? state : { ...state, updateWaiting: true };
44
+ case "update-applied":
45
+ return state.updateWaiting ? { ...state, updateWaiting: false } : state;
46
+ default: {
47
+ // Every member of PwaClientEventType is handled above. Should client-runtime ever add one, this turns into a
48
+ // compile error here rather than a silently ignored event at runtime.
49
+ const unreachable = event.type;
50
+ return unreachable;
51
+ }
52
+ }
53
+ }
54
+ /**
55
+ * The message every method rejects with during server-side rendering. The Vue package carries the same text, and
56
+ * the server-side parity suite in examples-browser-e2e compares the two.
57
+ */
58
+ export const SERVER_RENDERING_ERROR = "PWA methods are unavailable during server-side rendering: call them in the browser after hydration";
59
+ function rejectOnServer() {
60
+ return Promise.reject(new Error(SERVER_RENDERING_ERROR));
61
+ }
62
+ /**
63
+ * What the binding hands out on the server (ADR-0016, 2026-09-17 amendment). There is no service worker, no install
64
+ * prompt and no facade there, so every method rejects at once — waiting for a facade that will never be attached
65
+ * would leave the call pending forever, and touching a browser API would throw something far less clear.
66
+ */
67
+ export const SERVER_METHODS = Object.freeze({
68
+ register: rejectOnServer,
69
+ promptInstall: rejectOnServer,
70
+ applyUpdate: rejectOnServer,
71
+ logout: rejectOnServer,
72
+ checkForUpdate: rejectOnServer,
73
+ });
74
+ /**
75
+ * The message `PwaProvider` throws when both `client` and `updateCheck` are passed. The Vue package carries the
76
+ * same text (spec: 修订:主动检查更新, 框架绑定增量), and each package's test suite asserts it word for word.
77
+ */
78
+ export const CLIENT_AND_UPDATE_CHECK_ERROR = "Cannot pass both client and updateCheck: an injected client is already built, so the binding has no way to configure its automatic update check";
79
+ export function createStore() {
80
+ const listeners = new Set();
81
+ let state = INITIAL_STATE;
82
+ let client = null;
83
+ let unsubscribe = null;
84
+ /** Calls made while no facade is attached, released by the next `attach`. */
85
+ let waiters = [];
86
+ /**
87
+ * The attached facade — or, when none is attached yet, a promise for the next one.
88
+ *
89
+ * Waiting rather than throwing is what makes the binding usable from a child's mount effect. React runs effects
90
+ * child-first, so a component inside `PwaProvider` that registers on mount runs *before* the provider's own effect
91
+ * attaches the facade. Throwing here made that — the most natural way to write it — fail silently: the rejection
92
+ * was swallowed and nothing ever registered. The end-to-end suite caught it on its first real render.
93
+ *
94
+ * The same gap reopens under StrictMode's deliberate remount (detach, then children's effects, then attach again),
95
+ * so a call made after a detach waits too. A call made after the provider is gone for good never settles; the
96
+ * component that made it is gone as well, so no rejection is left unhandled.
97
+ */
98
+ function bound() {
99
+ if (client !== null)
100
+ return Promise.resolve(client);
101
+ return new Promise((resolve) => {
102
+ waiters.push(resolve);
103
+ });
104
+ }
105
+ /** A copy, so a listener that unsubscribes during delivery does not disturb this dispatch. */
106
+ function emit() {
107
+ for (const listener of [...listeners])
108
+ listener();
109
+ }
110
+ return {
111
+ subscribe(onChange) {
112
+ listeners.add(onChange);
113
+ return () => {
114
+ listeners.delete(onChange);
115
+ };
116
+ },
117
+ getSnapshot() {
118
+ return state;
119
+ },
120
+ attach(next) {
121
+ // A second attach without an intervening detach would otherwise leak the first subscription. The provider
122
+ // pairs them correctly today, but a store that leaks under misuse is a trap for the next caller.
123
+ unsubscribe?.();
124
+ client = next;
125
+ // Release every call that arrived before this facade did.
126
+ const released = waiters;
127
+ waiters = [];
128
+ for (const release of released)
129
+ release(next);
130
+ unsubscribe = next.subscribe((event) => {
131
+ const previous = state;
132
+ state = reduce(state, event);
133
+ // Notify only on a real change. Waking React for an event that changed nothing would re-render the tree
134
+ // for no reason, and `getSnapshot` would hand back the very same object anyway.
135
+ if (state !== previous)
136
+ emit();
137
+ });
138
+ },
139
+ detach() {
140
+ unsubscribe?.();
141
+ unsubscribe = null;
142
+ client = null;
143
+ if (state !== INITIAL_STATE) {
144
+ state = INITIAL_STATE;
145
+ emit();
146
+ }
147
+ },
148
+ // Each call waits for a facade (see `bound`), then forwards unchanged: return values and rejections are the
149
+ // facade's own.
150
+ register: async () => (await bound()).register(),
151
+ promptInstall: async () => (await bound()).promptInstall(),
152
+ applyUpdate: async () => (await bound()).applyUpdate(),
153
+ logout: async () => (await bound()).logout(),
154
+ checkForUpdate: async () => (await bound()).checkForUpdate(),
155
+ };
156
+ }
157
+ /**
158
+ * Creates (or takes) a facade, attaches it to the store, and returns the teardown. This is the entire body of the
159
+ * provider's effect, extracted so it can be unit tested without a renderer: the provider is then a one-liner with
160
+ * nowhere to hide a defect.
161
+ *
162
+ * `client` and `updateCheck` are positional optional parameters rather than fields on an options object on
163
+ * purpose. The caller holds them as `X | undefined` (they come from props), and under `exactOptionalPropertyTypes`
164
+ * an optional *property* rejects an explicitly passed `undefined`. Optional parameters carry no such restriction,
165
+ * so this avoids a `client?: PwaClient | undefined` signature that reads like a mistake.
166
+ *
167
+ * The returned teardown disposes the facade even when it was injected — the binding takes ownership for its
168
+ * lifetime, which keeps the Vue and React sides behaving alike. `PwaProvider` is responsible for rejecting a call
169
+ * that passes both `client` and `updateCheck` before this function is ever reached.
170
+ */
171
+ export function bindFacade(store, config, client, updateCheck) {
172
+ const facade = client ??
173
+ // Built conditionally, not `createPwaClient({ config, updateCheck })`: with `updateCheck` typed
174
+ // `UpdateCheckOption | undefined`, that object literal would write `X | undefined` into a property declared
175
+ // `updateCheck?: X`, which `exactOptionalPropertyTypes` rejects. The branch narrows it to `X`.
176
+ createPwaClient(updateCheck !== undefined ? { config, updateCheck } : { config });
177
+ store.attach(facade);
178
+ return () => {
179
+ store.detach();
180
+ facade.dispose();
181
+ };
182
+ }
183
+ /**
184
+ * The provider's `useEffect` dependency list, extracted for the same reason as `bindFacade`: this package ships no
185
+ * renderer, so nothing inside the component itself has a unit test. What matters here is the *shape* of the list
186
+ * React compares with `Object.is` — in particular that `updateCheck` contributes its `intervalMs` field and not
187
+ * the option object itself. An application that passes a fresh `{ intervalMs }` literal on every render must not
188
+ * tear the facade down and rebuild it just because that object's identity changed, exactly as `config`'s fields
189
+ * are read individually rather than depending on the `config` object.
190
+ */
191
+ export function providerEffectDeps(store, client, config, updateCheck) {
192
+ return [
193
+ store,
194
+ client,
195
+ config.appId,
196
+ config.scope,
197
+ config.serviceWorkerUrl,
198
+ config.updateMode,
199
+ config.installEnabled,
200
+ updateCheck?.intervalMs,
201
+ ];
202
+ }
203
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@pwa-platform/react",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "React 19 binding for PWA Platform.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "peerDependencies": {
19
+ "react": "^19.2.0"
20
+ },
21
+ "dependencies": {
22
+ "@pwa-platform/client-runtime": "0.1.0-beta.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/react": "19.3.0",
26
+ "react": "19.3.0",
27
+ "@pwa-platform/vue": "0.1.0-beta.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=22.0.0"
31
+ },
32
+ "publishConfig": {
33
+ "registry": "https://registry.npmjs.org/",
34
+ "access": "public",
35
+ "tag": "next"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc --project tsconfig.build.json",
39
+ "test": "vitest run",
40
+ "typecheck": "tsc --noEmit --project tsconfig.json"
41
+ }
42
+ }