@lowdefy/shell 0.0.0-experimental-20261019052246
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 +201 -0
- package/README.md +308 -0
- package/app/(t0,t1,t2,t3,t4)/[...path].js +38 -0
- package/app/(t0,t1,t2,t3,t4)/_layout.js +48 -0
- package/app/(t0,t1,t2,t3,t4)/index.js +38 -0
- package/app/+native-intent.js +30 -0
- package/app/_layout.js +33 -0
- package/app.config.ts +168 -0
- package/applyShellPlugins.js +43 -0
- package/authCallbackIntentFilters.js +46 -0
- package/babel.config.js +25 -0
- package/checkShellConfig.js +145 -0
- package/checkShellPlugins.js +74 -0
- package/getPrivacyManifest.js +39 -0
- package/metro.config.js +73 -0
- package/native-fingerprint.json +12 -0
- package/package.json +130 -0
- package/profiles.js +274 -0
- package/readShellConfig.js +36 -0
- package/resolveShellPlugins.js +109 -0
- package/runtimeVersion.js +105 -0
- package/scripts/checkReleaseBundle.mjs +358 -0
- package/scripts/createDayjsLocales.mjs +67 -0
- package/scripts/createShellAssets.mjs +54 -0
- package/scripts/createShellVersion.mjs +83 -0
- package/scripts/createTypeManifest.mjs +205 -0
- package/shell.dev.json +11 -0
- package/shellVariant.js +111 -0
- package/src/ShellContext.js +32 -0
- package/src/actions/CopyToClipboard.js +29 -0
- package/src/actions/CopyToClipboard.test.js +33 -0
- package/src/actions/GeolocationCurrentPosition.js +55 -0
- package/src/actions/GeolocationCurrentPosition.test.js +66 -0
- package/src/actions/GetLocalStorage.js +36 -0
- package/src/actions/RemoveLocalStorage.js +31 -0
- package/src/actions/ScrollTo.js +66 -0
- package/src/actions/ScrollTo.test.js +83 -0
- package/src/actions/SetFocus.js +33 -0
- package/src/actions/SetFocus.test.js +49 -0
- package/src/actions/SetLocalStorage.js +34 -0
- package/src/actions/localStorage.test.js +68 -0
- package/src/actions/measureInScrollView.js +36 -0
- package/src/actions/nativeActions.js +41 -0
- package/src/auth/AuthButton.js +44 -0
- package/src/auth/AuthGate.js +57 -0
- package/src/auth/AuthLink.js +43 -0
- package/src/auth/LockScreen.js +67 -0
- package/src/auth/LockScreen.test.js +37 -0
- package/src/auth/SignInScreen.js +256 -0
- package/src/auth/SignInScreen.test.js +182 -0
- package/src/auth/createAppAuth.js +306 -0
- package/src/auth/createAppAuth.test.js +414 -0
- package/src/auth/createAuthApi.js +99 -0
- package/src/auth/createAuthApi.test.js +135 -0
- package/src/auth/createCoreAuth.js +96 -0
- package/src/auth/createCoreAuth.test.js +188 -0
- package/src/auth/createSessionStores.js +45 -0
- package/src/auth/createTokenStore.js +103 -0
- package/src/auth/createTokenStore.test.js +116 -0
- package/src/auth/getAuthSettings.js +24 -0
- package/src/auth/getAuthView.js +39 -0
- package/src/auth/getAuthView.test.js +52 -0
- package/src/auth/getSheetReturnUrl.js +35 -0
- package/src/auth/getTokenStoreOptions.js +46 -0
- package/src/auth/isBackgroundShell.js +26 -0
- package/src/auth/isSheetReturnLink.js +35 -0
- package/src/auth/parseQuery.js +39 -0
- package/src/auth/purgeAppData.js +45 -0
- package/src/auth/shouldLock.js +26 -0
- package/src/auth/signInWithSheet.js +97 -0
- package/src/auth/signInWithSheet.test.js +205 -0
- package/src/auth/styles.js +78 -0
- package/src/auth/unregisterPushToken.js +21 -0
- package/src/auth/usesBiometricUnlock.js +23 -0
- package/src/auth/watchSession.js +47 -0
- package/src/auth/withoutCookies.js +26 -0
- package/src/cache/createSqliteCacheStore.js +113 -0
- package/src/cache/createSqliteCacheStore.test.js +93 -0
- package/src/cache/getCacheKey.js +25 -0
- package/src/cache/getLastCacheKeyName.js +22 -0
- package/src/components/AppProvider.js +90 -0
- package/src/components/Frame.js +116 -0
- package/src/components/Frame.test.js +101 -0
- package/src/components/LoadingScreen.js +35 -0
- package/src/components/MenuButton.js +108 -0
- package/src/components/MenuButton.test.js +51 -0
- package/src/components/MessageScreen.js +51 -0
- package/src/components/NavigationTheme.js +33 -0
- package/src/components/NoConnectionBanner.js +51 -0
- package/src/components/NoConnectionBanner.test.js +73 -0
- package/src/components/OtherPageScreen.js +56 -0
- package/src/components/PageScreen.js +244 -0
- package/src/components/PageScreen.test.js +492 -0
- package/src/components/ShellRoot.js +34 -0
- package/src/components/ShellRoot.test.js +62 -0
- package/src/components/ShellThemeProvider.js +45 -0
- package/src/components/SwitchToTab.js +36 -0
- package/src/components/SwitchToTab.test.js +63 -0
- package/src/components/TabIconImages.js +72 -0
- package/src/components/UpdateScreen.js +36 -0
- package/src/components/getNavigationTheme.js +44 -0
- package/src/components/getNavigationTheme.test.js +41 -0
- package/src/components/styles.js +50 -0
- package/src/components/useSurfaceTokens.js +47 -0
- package/src/config/getAppApiBase.js +31 -0
- package/src/config/getShellConfig.js +35 -0
- package/src/config/getShellConfig.test.js +80 -0
- package/src/createShellApp.js +105 -0
- package/src/dev/DevShell.js +228 -0
- package/src/dev/DevShell.test.js +233 -0
- package/src/dev/connect/ConfirmSheet.js +71 -0
- package/src/dev/connect/ConnectScreen.js +117 -0
- package/src/dev/connect/QrScanner.js +89 -0
- package/src/dev/connection/createDevShellConfig.js +47 -0
- package/src/dev/connection/createDevTransport.js +51 -0
- package/src/dev/connection/createDevTransport.test.js +87 -0
- package/src/dev/connection/createServerList.js +48 -0
- package/src/dev/connection/isLoopbackHost.js +25 -0
- package/src/dev/connection/pairedShellApp.test.js +104 -0
- package/src/dev/connection/parseDevLink.js +66 -0
- package/src/dev/connection/parseDevLink.test.js +97 -0
- package/src/dev/connection/parseQuery.js +33 -0
- package/src/dev/connection/parseServerInput.js +40 -0
- package/src/dev/connection/parseServerUrl.js +45 -0
- package/src/dev/devStyles.js +97 -0
- package/src/dev/index.js +21 -0
- package/src/dev/inject/createDeviceInjections.js +185 -0
- package/src/dev/inject/createDeviceInjections.test.js +208 -0
- package/src/dev/inject/createOfflineFetch.js +35 -0
- package/src/dev/inject/createOfflineFetch.test.js +68 -0
- package/src/dev/menu/BlockGallery.js +33 -0
- package/src/dev/menu/DevMenu.js +121 -0
- package/src/dev/menu/createLogBuffer.js +83 -0
- package/src/dev/menu/watchShake.js +52 -0
- package/src/dev/menu/watchShake.test.js +61 -0
- package/src/dev/recorder/createNativeRecorder.js +204 -0
- package/src/dev/recorder/createNativeRecorder.test.js +307 -0
- package/src/dev/redirectDevLink.js +30 -0
- package/src/dev/redirectDevLink.test.js +49 -0
- package/src/dev/steps/createStepRunner.js +184 -0
- package/src/dev/steps/createStepRunner.test.js +262 -0
- package/src/dev/steps/findBlock.js +78 -0
- package/src/dev/steps/isContextSettled.js +37 -0
- package/src/dev/steps/waitForSettle.js +39 -0
- package/src/dev/steps/waitUntil.js +40 -0
- package/src/dev/tab/DevTabClient.js +97 -0
- package/src/dev/tab/answerEval.js +39 -0
- package/src/dev/tab/answerInspect.js +45 -0
- package/src/dev/tab/answerLoadState.js +50 -0
- package/src/dev/tab/createDevTabClient.js +277 -0
- package/src/dev/tab/createDevTabClient.test.js +314 -0
- package/src/dev/tab/devClientMarker.js +22 -0
- package/src/dev/tab/devClientMarker.test.js +23 -0
- package/src/dev/tab/findPageContext.js +43 -0
- package/src/dev/tab/getShownPage.js +42 -0
- package/src/dev/tab/readEventStream.js +68 -0
- package/src/dev/tab/reloadApp.js +42 -0
- package/src/dev/tab/reloadApp.test.js +115 -0
- package/src/device/injected.js +60 -0
- package/src/device/injected.test.js +39 -0
- package/src/host/createDeviceStore.js +32 -0
- package/src/host/createMemoryStore.js +32 -0
- package/src/host/createNetworkStatus.js +94 -0
- package/src/host/createNetworkStatus.test.js +94 -0
- package/src/host/createRouterAdapter.js +33 -0
- package/src/host/createShellFetch.js +60 -0
- package/src/host/createShellFetch.test.js +150 -0
- package/src/host/createShellGlobals.js +62 -0
- package/src/host/createShellHost.js +73 -0
- package/src/host/createShellHost.test.js +36 -0
- package/src/host/getOrigin.js +26 -0
- package/src/host/isOnline.js +22 -0
- package/src/host/networkStatus.js +24 -0
- package/src/host/withShellHeader.js +25 -0
- package/src/host/withShellHeader.test.js +28 -0
- package/src/js/evaluateJsEntries.js +43 -0
- package/src/js/evaluateJsEntries.test.js +48 -0
- package/src/locale/createShellLocale.js +112 -0
- package/src/locale/createShellLocale.test.js +74 -0
- package/src/locale/dayjsLocales.js +165 -0
- package/src/locale/getDeviceLocales.js +23 -0
- package/src/locale/loadDayjsLocale.js +32 -0
- package/src/locale/resolveActiveLocale.js +46 -0
- package/src/locale/resolveActiveLocale.test.js +40 -0
- package/src/navigation/SlotContext.js +24 -0
- package/src/navigation/frameSlots.js +27 -0
- package/src/navigation/getFrame.js +30 -0
- package/src/navigation/getInitialScreenOptions.js +63 -0
- package/src/navigation/getInitialScreenOptions.test.js +66 -0
- package/src/navigation/getPresentation.js +28 -0
- package/src/navigation/getSlotRootPath.js +27 -0
- package/src/navigation/getStackOptions.js +185 -0
- package/src/navigation/getTabHref.js +45 -0
- package/src/navigation/getTabIcons.js +43 -0
- package/src/navigation/isPageUnderHeader.js +46 -0
- package/src/navigation/navigation.test.js +342 -0
- package/src/navigation/toRouterHref.js +29 -0
- package/src/plugins/appTypes.js +28 -0
- package/src/plugins/prefixTypes.js +28 -0
- package/src/plugins/shellPlugins.json +14 -0
- package/src/plugins/typeManifest.js +649 -0
- package/src/plugins/types.js +61 -0
- package/src/plugins/types.test.js +173 -0
- package/src/polyfills/installGetRandomValues.js +28 -0
- package/src/session/createAppSession.js +318 -0
- package/src/session/createAppSession.test.js +359 -0
- package/src/session/getPageView.js +59 -0
- package/src/session/getPageView.test.js +137 -0
- package/src/sync/encodePagePath.js +27 -0
- package/src/sync/fetchPage.js +49 -0
- package/src/sync/fetchRoot.js +38 -0
- package/src/sync/fetchUser.js +28 -0
- package/src/sync/runLimited.js +36 -0
- package/src/sync/toRootConfig.js +35 -0
- package/src/test/createFakeNetwork.js +40 -0
- package/src/test/createFakeSecureStore.js +40 -0
- package/src/test/createFakeServer.js +98 -0
- package/src/test/createMemoryCacheStore.js +59 -0
- package/src/test/expoImage.js +38 -0
- package/src/test/expoSpeechRecognition.js +31 -0
- package/src/test/expoUi.js +55 -0
- package/src/test/expoUiBottomSheet.js +64 -0
- package/src/test/expoUiDateTimePicker.js +26 -0
- package/src/test/expoUiMenu.js +46 -0
- package/src/test/expoUiSegmentedControl.js +47 -0
- package/src/test/expoUiSwiftUi.js +62 -0
- package/src/test/expoUiSwiftUiModifiers.js +23 -0
- package/src/test/expoWebBrowser.js +23 -0
- package/src/test/fixtures.js +98 -0
- package/src/test/renderPageScreen.js +109 -0
- package/src/types/checkTypeSet.js +39 -0
- package/src/types/collectPageTypes.js +49 -0
- package/src/types/collectPageTypes.test.js +90 -0
- package/src/types/findMissingPageTypes.js +40 -0
- package/src/version/compareVersions.js +38 -0
- package/src/version/isShellTooOld.js +30 -0
- package/src/version/shellVersion.js +21 -0
- package/src/version/shellVersion.test.js +32 -0
- package/withExcludedNativeModules.js +93 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
import { getTrace } from '@lowdefy/engine';
|
|
19
|
+
|
|
20
|
+
// The page instance the shell shows, `{ pageId, pathParams, instanceKey }`, read from the route the
|
|
21
|
+
// focused screen set (host globals), as the trace reads it. A screen's page context is keyed by its
|
|
22
|
+
// route instance too (engine getContext), so the key ends in `@{instanceId}`. Null before the first
|
|
23
|
+
// page renders.
|
|
24
|
+
function getShownPage(lowdefy) {
|
|
25
|
+
const globals = lowdefy?._internal?.globals;
|
|
26
|
+
if (type.isNone(globals)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const entry = getTrace(lowdefy).pathEntryOf(globals.window.location.href);
|
|
30
|
+
if (type.isNone(entry)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
// Only the shell's own globals know the focused screen; a page rendered without one has one
|
|
34
|
+
// context per page instance.
|
|
35
|
+
const instanceId = globals.shownScreen?.instanceId;
|
|
36
|
+
const instanceKey = type.isNone(instanceId)
|
|
37
|
+
? entry.instanceKey
|
|
38
|
+
: `${entry.instanceKey}@${instanceId}`;
|
|
39
|
+
return { pageId: entry.pageId, pathParams: entry.pathParams, instanceKey };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default getShownPage;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reads a text/event-stream body (the dev server's /api/reload) and calls onEvent with each event
|
|
19
|
+
* as `{ event, data }`, the way EventSource dispatches them. React Native has no EventSource, so
|
|
20
|
+
* the stream is read from a streaming fetch body. Resolves when the stream ends.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} params
|
|
23
|
+
* @param {ReadableStream} params.body The response body.
|
|
24
|
+
* @param {(event: { event: string, data: string }) => void} params.onEvent
|
|
25
|
+
*/
|
|
26
|
+
async function readEventStream({ body, onEvent }) {
|
|
27
|
+
const reader = body.getReader();
|
|
28
|
+
const decoder = new TextDecoder();
|
|
29
|
+
let buffer = '';
|
|
30
|
+
let event = null;
|
|
31
|
+
let dataLines = [];
|
|
32
|
+
|
|
33
|
+
function readLine(line) {
|
|
34
|
+
if (line === '') {
|
|
35
|
+
if (dataLines.length > 0 || event !== null) {
|
|
36
|
+
onEvent({ event: event ?? 'message', data: dataLines.join('\n') });
|
|
37
|
+
}
|
|
38
|
+
event = null;
|
|
39
|
+
dataLines = [];
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (line.startsWith(':')) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const colon = line.indexOf(':');
|
|
46
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
47
|
+
const value = colon === -1 ? '' : line.slice(colon + 1).replace(/^ /, '');
|
|
48
|
+
if (field === 'event') {
|
|
49
|
+
event = value;
|
|
50
|
+
}
|
|
51
|
+
if (field === 'data') {
|
|
52
|
+
dataLines.push(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (;;) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
buffer += decoder.decode(value, { stream: true });
|
|
62
|
+
const lines = buffer.split(/\r\n|\r|\n/);
|
|
63
|
+
buffer = lines.pop();
|
|
64
|
+
lines.forEach(readLine);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default readEventStream;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import matchPagePath from '@lowdefy/node-utils/matchPagePath.js';
|
|
18
|
+
|
|
19
|
+
import encodePagePath from '../../sync/encodePagePath.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The dev server rebuilt the app: refetch the root and the page on screen, then rebuild every
|
|
23
|
+
* screen with empty page state, as a browser tab does on a reload. The shell stays on the page it
|
|
24
|
+
* shows.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} params
|
|
27
|
+
* @param {Object} params.session The app session (createAppSession).
|
|
28
|
+
*/
|
|
29
|
+
async function reloadApp({ session }) {
|
|
30
|
+
const { root } = session.getState();
|
|
31
|
+
const path = session.getCurrentPath().replace(/^\//, '').split('?')[0];
|
|
32
|
+
const match =
|
|
33
|
+
root === null ? null : matchPagePath({ routes: root.pageIndex, path: encodePagePath(path) });
|
|
34
|
+
if (match !== null) {
|
|
35
|
+
const entry = root.pageIndex.find((indexEntry) => indexEntry.pageId === match.pageId);
|
|
36
|
+
await session.loadPage({ entry, path });
|
|
37
|
+
}
|
|
38
|
+
// Syncs the root, then starts a new generation: new page contexts on the current route.
|
|
39
|
+
await session.handleBuildChanged();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default reloadApp;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { jest } from '@jest/globals';
|
|
18
|
+
import { act, screen } from '@testing-library/react-native';
|
|
19
|
+
|
|
20
|
+
import getShownPage from './getShownPage.js';
|
|
21
|
+
import reloadApp from './reloadApp.js';
|
|
22
|
+
import renderPageScreen from '../../test/renderPageScreen.js';
|
|
23
|
+
import { createRoot } from '../../test/fixtures.js';
|
|
24
|
+
|
|
25
|
+
// The native stack's header height, which a Screen's headerBottom pins under on iOS.
|
|
26
|
+
jest.mock('expo-router/native-stack', () => ({ useAnimatedHeaderHeight: () => 0 }));
|
|
27
|
+
jest.mock('react-native-safe-area-context', () => ({
|
|
28
|
+
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 34, left: 0 }),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('expo-router', () => ({
|
|
32
|
+
DarkTheme: { fonts: {} },
|
|
33
|
+
DefaultTheme: { fonts: {} },
|
|
34
|
+
ThemeProvider: ({ children }) => children,
|
|
35
|
+
router: { back: jest.fn(), canGoBack: () => false },
|
|
36
|
+
useFocusEffect: (effect) => {
|
|
37
|
+
const { useEffect: useReactEffect } = jest.requireActual('react');
|
|
38
|
+
useReactEffect(effect, [effect]);
|
|
39
|
+
},
|
|
40
|
+
useIsFocused: () => true,
|
|
41
|
+
useNavigation: () => ({ setOptions: jest.fn() }),
|
|
42
|
+
useRoute: () => ({ key: 'route-1' }),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
function todayPage(greeting) {
|
|
46
|
+
return {
|
|
47
|
+
id: 'page:today',
|
|
48
|
+
pageId: 'today',
|
|
49
|
+
blockId: 'today',
|
|
50
|
+
type: 'Screen',
|
|
51
|
+
properties: { title: 'Today' },
|
|
52
|
+
slots: {
|
|
53
|
+
content: {
|
|
54
|
+
blocks: [
|
|
55
|
+
{
|
|
56
|
+
id: 'block:today:greeting:0',
|
|
57
|
+
blockId: 'greeting',
|
|
58
|
+
type: 'Box',
|
|
59
|
+
properties: { content: greeting },
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A dev root: its pages have no hash, so the shell fetches them on each visit.
|
|
68
|
+
async function renderDevPage() {
|
|
69
|
+
const { server, session } = await renderPageScreen({
|
|
70
|
+
path: 'today',
|
|
71
|
+
root: createRoot({
|
|
72
|
+
pageIndex: [{ path: 'today', pageId: 'today', hash: null, typesKey: 'screen' }],
|
|
73
|
+
typeSets: {
|
|
74
|
+
screen: {
|
|
75
|
+
blocks: [
|
|
76
|
+
{ type: 'Box', package: '@lowdefy/blocks-native', version: 7 },
|
|
77
|
+
{ type: 'Screen', package: '@lowdefy/blocks-native', version: 7 },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
pages: { today: { pageConfig: todayPage('Hello'), etag: '"p1"' } },
|
|
83
|
+
});
|
|
84
|
+
await screen.findByText('Hello');
|
|
85
|
+
return { server, session };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
test('a reload refetches the root and the page on screen, and rebuilds it with empty state', async () => {
|
|
89
|
+
const { server, session } = await renderDevPage();
|
|
90
|
+
const before = session.getState().lowdefy;
|
|
91
|
+
const shown = getShownPage(before);
|
|
92
|
+
// The screen's page context is keyed by its route instance.
|
|
93
|
+
expect(shown).toEqual({ pageId: 'today', pathParams: {}, instanceKey: 'page:today@route-1' });
|
|
94
|
+
before.contexts[shown.instanceKey]._internal.State.set('typed', 'Quarterly');
|
|
95
|
+
expect(before.contexts[shown.instanceKey].state.typed).toBe('Quarterly');
|
|
96
|
+
|
|
97
|
+
server.state.pages.today = { pageConfig: todayPage('Hello again'), etag: '"p2"' };
|
|
98
|
+
server.state.rootEtag = '"r2"';
|
|
99
|
+
server.fetch.mockClear();
|
|
100
|
+
await act(async () => {
|
|
101
|
+
await reloadApp({ session });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const paths = server.fetch.mock.calls.map(([url]) => new URL(url).pathname);
|
|
105
|
+
// The rebuilt screen revalidates its page, as every screen does after a rebuild, so a screen
|
|
106
|
+
// under the one shown gets its new version too: the version reloadApp fetched is current.
|
|
107
|
+
expect(paths).toEqual(['/api/page/today', '/api/mobile/root', '/api/user', '/api/page/today']);
|
|
108
|
+
const revalidation = server.fetch.mock.calls.at(-1)[1];
|
|
109
|
+
expect(revalidation.headers).toEqual({ 'If-None-Match': '"p2"' });
|
|
110
|
+
expect(await screen.findByText('Hello again')).toBeTruthy();
|
|
111
|
+
expect(session.getCurrentPath()).toBe('/today');
|
|
112
|
+
const after = session.getState().lowdefy;
|
|
113
|
+
expect(after).not.toBe(before);
|
|
114
|
+
expect(after.contexts[shown.instanceKey].state.typed).toBeUndefined();
|
|
115
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// The injector Lowdefy Dev installs (src/dev/inject). A release shell never installs one.
|
|
18
|
+
let injector = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The next device result Lowdefy Dev was told to fake, or undefined when there is none: in a
|
|
22
|
+
* release shell always. A device action reads it first, in one line, and uses the device only when
|
|
23
|
+
* nothing was injected:
|
|
24
|
+
*
|
|
25
|
+
* const scan = injected('scan');
|
|
26
|
+
* if (scan !== undefined) return scan;
|
|
27
|
+
*
|
|
28
|
+
* Names:
|
|
29
|
+
* - `scan`: the next ScanCode or QRScanner read, `{ data, type }`.
|
|
30
|
+
* - `photo`: the next TakePhoto or PickImage result, `{ base64, mimeType, width, height }`.
|
|
31
|
+
* - `nfc`: the next ReadNfc read.
|
|
32
|
+
* - `transcript`: what the next dictation session hears, `{ partials?, final }` or `{ error }`.
|
|
33
|
+
* Dictation (blocks-native) reads it through the source Lowdefy Dev installs with
|
|
34
|
+
* installDictationSource.
|
|
35
|
+
* - `location`: the location GeolocationCurrentPosition and geofences read,
|
|
36
|
+
* `{ latitude, longitude, accuracy, ... }`.
|
|
37
|
+
* - `permission:<name>`: the answer to the next request for a permission, 'granted' or 'denied'
|
|
38
|
+
* (`permission:microphone` for dictation).
|
|
39
|
+
*
|
|
40
|
+
* @param {string} name
|
|
41
|
+
*/
|
|
42
|
+
function injected(name) {
|
|
43
|
+
if (injector === null) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return injector.take(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Installs the injector `injected` reads (`{ take(name) }`); returns the function that removes it.
|
|
50
|
+
function installInjector(next) {
|
|
51
|
+
injector = next;
|
|
52
|
+
return () => {
|
|
53
|
+
if (injector === next) {
|
|
54
|
+
injector = null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { installInjector };
|
|
60
|
+
export default injected;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import injected, { installInjector } from './injected.js';
|
|
18
|
+
|
|
19
|
+
test('with no injector installed, as in a release shell, nothing is injected', () => {
|
|
20
|
+
expect(injected('scan')).toBeUndefined();
|
|
21
|
+
expect(injected('permission:camera')).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('injected reads the installed injector until it is removed', () => {
|
|
25
|
+
const take = (name) => `next ${name}`;
|
|
26
|
+
const remove = installInjector({ take });
|
|
27
|
+
expect(injected('scan')).toBe('next scan');
|
|
28
|
+
remove();
|
|
29
|
+
expect(injected('scan')).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('removing an injector that was replaced leaves the newer one installed', () => {
|
|
33
|
+
const removeFirst = installInjector({ take: () => 'first' });
|
|
34
|
+
const removeSecond = installInjector({ take: () => 'second' });
|
|
35
|
+
removeFirst();
|
|
36
|
+
expect(injected('scan')).toBe('second');
|
|
37
|
+
removeSecond();
|
|
38
|
+
expect(injected('scan')).toBeUndefined();
|
|
39
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Storage } from 'expo-sqlite/kv-store';
|
|
18
|
+
|
|
19
|
+
// The host store that lasts on the device ({ get, set, remove, keys }), over SQLite's key-value
|
|
20
|
+
// store. The core namespaces its keys per app and user.
|
|
21
|
+
function createDeviceStore() {
|
|
22
|
+
return {
|
|
23
|
+
get: (key) => Storage.getItemSync(key),
|
|
24
|
+
set: (key, value) => Storage.setItemSync(key, String(value)),
|
|
25
|
+
remove: (key) => {
|
|
26
|
+
Storage.removeItemSync(key);
|
|
27
|
+
},
|
|
28
|
+
keys: () => Storage.getAllKeysSync(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default createDeviceStore;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// A host store held in memory, for the session store: it lasts as long as the app runs.
|
|
18
|
+
function createMemoryStore() {
|
|
19
|
+
const stored = new Map();
|
|
20
|
+
return {
|
|
21
|
+
get: (key) => stored.get(key) ?? null,
|
|
22
|
+
set: (key, value) => {
|
|
23
|
+
stored.set(key, String(value));
|
|
24
|
+
},
|
|
25
|
+
remove: (key) => {
|
|
26
|
+
stored.delete(key);
|
|
27
|
+
},
|
|
28
|
+
keys: () => [...stored.keys()],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default createMemoryStore;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import isOnline from './isOnline.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The shell's one online state. The NoConnection banner and the app's `online` and `offline`
|
|
21
|
+
* events (createShellHost) read it, and two sources feed it:
|
|
22
|
+
* - the phone's network, through expo-network, read while anything subscribes;
|
|
23
|
+
* - Lowdefy Dev's network injection (`setInjectedOffline`), since a simulator or emulator shares
|
|
24
|
+
* the computer's network and cannot go offline itself. A release shell never sets it.
|
|
25
|
+
*
|
|
26
|
+
* The shell is online when the phone's network is up and nothing injected offline. Subscribers
|
|
27
|
+
* hear only changes, as a browser fires `online` and `offline`.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} params
|
|
30
|
+
* @param {Object} params.network expo-network: `addNetworkStateListener` and
|
|
31
|
+
* `getNetworkStateAsync`.
|
|
32
|
+
*/
|
|
33
|
+
function createNetworkStatus({ network }) {
|
|
34
|
+
const listeners = new Set();
|
|
35
|
+
let deviceOnline = true;
|
|
36
|
+
let injectedOffline = false;
|
|
37
|
+
let online = true;
|
|
38
|
+
let stopListening = null;
|
|
39
|
+
|
|
40
|
+
function settle() {
|
|
41
|
+
const next = deviceOnline && !injectedOffline;
|
|
42
|
+
if (next === online) return;
|
|
43
|
+
online = next;
|
|
44
|
+
[...listeners].forEach((listener) => listener(online));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setDeviceState(state) {
|
|
48
|
+
deviceOnline = isOnline(state);
|
|
49
|
+
settle();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Reads the phone's network until stopped. A change heard before the first read answers is
|
|
53
|
+
// newer than that read, so the read is then dropped.
|
|
54
|
+
function listen() {
|
|
55
|
+
let heard = false;
|
|
56
|
+
let active = true;
|
|
57
|
+
const subscription = network.addNetworkStateListener((state) => {
|
|
58
|
+
heard = true;
|
|
59
|
+
setDeviceState(state);
|
|
60
|
+
});
|
|
61
|
+
network.getNetworkStateAsync().then((state) => {
|
|
62
|
+
if (active && !heard) setDeviceState(state);
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
active = false;
|
|
66
|
+
subscription.remove();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Calls the listener with the new online state on every change; returns the function that
|
|
71
|
+
// stops it.
|
|
72
|
+
function subscribe(listener) {
|
|
73
|
+
if (listeners.size === 0) {
|
|
74
|
+
stopListening = listen();
|
|
75
|
+
}
|
|
76
|
+
listeners.add(listener);
|
|
77
|
+
return () => {
|
|
78
|
+
if (!listeners.delete(listener)) return;
|
|
79
|
+
if (listeners.size === 0) {
|
|
80
|
+
stopListening();
|
|
81
|
+
stopListening = null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setInjectedOffline(offline) {
|
|
87
|
+
injectedOffline = offline;
|
|
88
|
+
settle();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { isOnline: () => online, setInjectedOffline, subscribe };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default createNetworkStatus;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
import createFakeNetwork, { OFFLINE, ONLINE } from '../test/createFakeNetwork.js';
|
|
20
|
+
import createNetworkStatus from './createNetworkStatus.js';
|
|
21
|
+
|
|
22
|
+
// Lets the first read of the phone's network answer.
|
|
23
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
24
|
+
|
|
25
|
+
test('the phone going offline and back is heard by subscribers, once per change', async () => {
|
|
26
|
+
const network = createFakeNetwork();
|
|
27
|
+
const status = createNetworkStatus({ network });
|
|
28
|
+
const listener = jest.fn();
|
|
29
|
+
status.subscribe(listener);
|
|
30
|
+
await settle();
|
|
31
|
+
expect(status.isOnline()).toBe(true);
|
|
32
|
+
network.change(OFFLINE);
|
|
33
|
+
network.change(OFFLINE);
|
|
34
|
+
expect(status.isOnline()).toBe(false);
|
|
35
|
+
network.change(ONLINE);
|
|
36
|
+
expect(listener.mock.calls).toEqual([[false], [true]]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('the first read of the phone network sets the state', async () => {
|
|
40
|
+
const status = createNetworkStatus({ network: createFakeNetwork({ state: OFFLINE }) });
|
|
41
|
+
const listener = jest.fn();
|
|
42
|
+
status.subscribe(listener);
|
|
43
|
+
expect(status.isOnline()).toBe(true);
|
|
44
|
+
await settle();
|
|
45
|
+
expect(status.isOnline()).toBe(false);
|
|
46
|
+
expect(listener).toHaveBeenCalledWith(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a change heard before the first read answers wins over that read', async () => {
|
|
50
|
+
const network = createFakeNetwork({ state: OFFLINE });
|
|
51
|
+
const status = createNetworkStatus({ network });
|
|
52
|
+
status.subscribe(() => undefined);
|
|
53
|
+
network.change(ONLINE);
|
|
54
|
+
network.getNetworkStateAsync = async () => OFFLINE;
|
|
55
|
+
await settle();
|
|
56
|
+
expect(status.isOnline()).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('injected offline takes the shell offline while the phone is online, until it is lifted', async () => {
|
|
60
|
+
const status = createNetworkStatus({ network: createFakeNetwork() });
|
|
61
|
+
const listener = jest.fn();
|
|
62
|
+
status.subscribe(listener);
|
|
63
|
+
await settle();
|
|
64
|
+
status.setInjectedOffline(true);
|
|
65
|
+
expect(status.isOnline()).toBe(false);
|
|
66
|
+
status.setInjectedOffline(false);
|
|
67
|
+
expect(status.isOnline()).toBe(true);
|
|
68
|
+
expect(listener.mock.calls).toEqual([[false], [true]]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('lifting injected offline leaves the shell offline while the phone is offline', async () => {
|
|
72
|
+
const network = createFakeNetwork();
|
|
73
|
+
const status = createNetworkStatus({ network });
|
|
74
|
+
status.subscribe(() => undefined);
|
|
75
|
+
await settle();
|
|
76
|
+
status.setInjectedOffline(true);
|
|
77
|
+
network.change(OFFLINE);
|
|
78
|
+
status.setInjectedOffline(false);
|
|
79
|
+
expect(status.isOnline()).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('the phone network is read only while something subscribes', async () => {
|
|
83
|
+
const network = createFakeNetwork();
|
|
84
|
+
const status = createNetworkStatus({ network });
|
|
85
|
+
expect(network.listening()).toBe(false);
|
|
86
|
+
const first = status.subscribe(() => undefined);
|
|
87
|
+
const second = status.subscribe(() => undefined);
|
|
88
|
+
expect(network.listening()).toBe(true);
|
|
89
|
+
first();
|
|
90
|
+
first();
|
|
91
|
+
expect(network.listening()).toBe(true);
|
|
92
|
+
second();
|
|
93
|
+
expect(network.listening()).toBe(false);
|
|
94
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { router } from 'expo-router';
|
|
18
|
+
|
|
19
|
+
import toRouterHref from '../navigation/toRouterHref.js';
|
|
20
|
+
|
|
21
|
+
// The core's router contract (push, replace, back) on Expo Router.
|
|
22
|
+
function createRouterAdapter({ session }) {
|
|
23
|
+
function href(target) {
|
|
24
|
+
return toRouterHref({ ...target, root: session.getState().root });
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
push: (target) => router.push(href(target)),
|
|
28
|
+
replace: (target) => router.replace(href(target)),
|
|
29
|
+
back: () => router.back(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default createRouterAdapter;
|