@lowdefy/shell 3.23.2 → 7.2.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 +183 -183
- package/README.md +364 -2
- 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 +39 -0
- package/app/_layout.js +36 -0
- package/app.config.ts +183 -0
- package/appLinks.js +65 -0
- package/applyShellPlugins.js +43 -0
- package/authCallbackIntentFilters.js +46 -0
- package/babel.config.js +25 -0
- package/checkShellConfig.js +172 -0
- package/checkShellPlugins.js +74 -0
- package/getPrivacyManifest.js +39 -0
- package/metro.config.js +73 -0
- package/native/LowdefyAppShortcuts.swift.template +84 -0
- package/native/withAppShortcuts.js +60 -0
- package/native-fingerprint.json +16 -0
- package/package.json +120 -40
- package/profiles.js +607 -0
- package/readShellConfig.js +36 -0
- package/resolveShellPlugins.js +109 -0
- package/runtimeVersion.js +112 -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 +126 -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 +61 -0
- package/src/actions/GeolocationCurrentPosition.test.js +79 -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 +268 -0
- package/src/auth/SignInScreen.test.js +187 -0
- package/src/auth/createAppAuth.js +325 -0
- package/src/auth/createAppAuth.test.js +424 -0
- package/src/auth/createAuthApi.js +115 -0
- package/src/auth/createAuthApi.test.js +138 -0
- package/src/auth/createCoreAuth.js +96 -0
- package/src/auth/createCoreAuth.test.js +188 -0
- package/src/auth/createDeviceKey.js +62 -0
- package/src/auth/createDeviceKey.test.js +54 -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 +80 -0
- package/src/auth/purgeAppData.test.js +104 -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 +62 -0
- package/src/auth/unregisterPushToken.test.js +67 -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 +118 -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 +121 -0
- package/src/components/MenuButton.test.js +74 -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 +95 -0
- package/src/components/OtherPageScreen.test.js +74 -0
- package/src/components/PageScreen.js +254 -0
- package/src/components/PageScreen.test.js +492 -0
- package/src/components/RouteErrorScreen.js +47 -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 +40 -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 +34 -0
- package/src/config/getShellConfig.js +37 -0
- package/src/config/getShellConfig.test.js +80 -0
- package/src/createShellApp.js +126 -0
- package/src/dev/DevShell.js +239 -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 +188 -0
- package/src/dev/inject/createDeviceInjections.test.js +224 -0
- package/src/dev/inject/createOfflineFetch.js +35 -0
- package/src/dev/inject/createOfflineFetch.test.js +68 -0
- package/src/dev/inject/injectPush.js +60 -0
- package/src/dev/inject/injectShare.js +55 -0
- package/src/dev/inject/withInjectedDevice.js +98 -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/createDevTabClient.js +272 -0
- package/src/dev/tab/createDevTabClient.test.js +317 -0
- package/src/dev/tab/describeTabError.js +35 -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/DeviceHosts.js +32 -0
- package/src/device/basic/createFilesFeature.js +41 -0
- package/src/device/basic/createHapticsFeature.js +46 -0
- package/src/device/basic/createKeepAwakeFeature.js +37 -0
- package/src/device/basic/createPhotosFeature.js +40 -0
- package/src/device/basic/createRequestQueueFeature.js +37 -0
- package/src/device/basic/createShareFeature.js +63 -0
- package/src/device/basic/createShareFeature.test.js +47 -0
- package/src/device/basic/createSpeechFeature.js +43 -0
- package/src/device/basic/createUploadFeature.js +41 -0
- package/src/device/basic/toImagePickerOptions.js +41 -0
- package/src/device/basic/toPickedFile.js +54 -0
- package/src/device/calendar/index.js +68 -0
- package/src/device/contacts/index.js +67 -0
- package/src/device/createDevice.js +65 -0
- package/src/device/device.js +27 -0
- package/src/device/field/MapView.js +93 -0
- package/src/device/field/ScanCodeSheet.js +86 -0
- package/src/device/field/ScannerView.js +52 -0
- package/src/device/field/createAudioFeature.js +70 -0
- package/src/device/field/createBrightnessFeature.js +40 -0
- package/src/device/field/createCameraFeature.js +56 -0
- package/src/device/field/createDocumentScannerFeature.js +51 -0
- package/src/device/field/createLocationFeature.js +39 -0
- package/src/device/field/createScanRequests.js +55 -0
- package/src/device/field/index.js +39 -0
- package/src/device/field/scanRequests.js +22 -0
- package/src/device/injected.js +60 -0
- package/src/device/injected.test.js +39 -0
- package/src/device/nfc/index.js +89 -0
- package/src/device/nfc/toNfcRecord.js +44 -0
- package/src/device/nfc/toNfcRecord.test.js +48 -0
- package/src/device/tracking/createBacklog.js +63 -0
- package/src/device/tracking/createBacklog.test.js +47 -0
- package/src/device/tracking/index.js +211 -0
- package/src/device/tracking/index.test.js +110 -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 +62 -0
- package/src/host/createShellFetch.test.js +165 -0
- package/src/host/createShellGlobals.js +62 -0
- package/src/host/createShellHost.js +83 -0
- package/src/host/createShellHost.test.js +36 -0
- package/src/host/getOrigin.js +31 -0
- package/src/host/isNetworkError.js +24 -0
- package/src/host/isOnline.js +22 -0
- package/src/host/networkStatus.js +24 -0
- package/src/host/readServerError.js +45 -0
- package/src/host/readServerError.test.js +85 -0
- package/src/host/reportShellError.js +47 -0
- package/src/host/reportShellError.test.js +79 -0
- package/src/host/withNetworkErrors.js +38 -0
- package/src/host/withNetworkErrors.test.js +61 -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/native/backgroundFetch.js +97 -0
- package/src/native/callEndpoint.js +63 -0
- package/src/native/createAppEventTrigger.js +70 -0
- package/src/native/currentApp.js +49 -0
- package/src/native/getPagePath.js +32 -0
- package/src/native/openPath.js +55 -0
- package/src/native/openPath.test.js +39 -0
- package/src/native/parseShellLink.js +38 -0
- package/src/native/parseShellLink.test.js +29 -0
- package/src/native/startAppServices.js +94 -0
- package/src/native/startAppServices.test.js +97 -0
- package/src/native/startShareIn.js +59 -0
- package/src/native/startShortcuts.js +88 -0
- package/src/native/startShortcuts.test.js +59 -0
- package/src/native/toSharePayload.js +47 -0
- package/src/native/toSharePayload.test.js +47 -0
- package/src/native/whenAppReady.js +48 -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/toAppLinkPath.js +43 -0
- package/src/navigation/toAppLinkPath.test.js +53 -0
- package/src/navigation/toRouterHref.js +29 -0
- package/src/notifications/createNotificationsFeature.js +61 -0
- package/src/notifications/getCategoryAction.js +31 -0
- package/src/notifications/handleNotificationResponse.js +82 -0
- package/src/notifications/registerPushToken.js +71 -0
- package/src/notifications/startNotifications.js +114 -0
- package/src/notifications/toCategoryActions.js +42 -0
- package/src/notifications/toNotificationEvent.js +36 -0
- package/src/notifications/toScheduleRequest.js +62 -0
- package/src/plugins/appTypes.js +28 -0
- package/src/plugins/prefixTypes.js +28 -0
- package/src/plugins/shellPlugins.json +15 -0
- package/src/plugins/typeManifest.js +789 -0
- package/src/plugins/types.js +64 -0
- package/src/plugins/types.test.js +173 -0
- package/src/polyfills/installGetRandomValues.js +28 -0
- package/src/queue/createRequestQueue.js +198 -0
- package/src/queue/createRequestQueue.test.js +215 -0
- package/src/queue/createSqliteQueueStore.js +121 -0
- package/src/queue/summarizeQueue.js +39 -0
- package/src/session/createAppSession.js +323 -0
- package/src/session/createAppSession.test.js +391 -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 +72 -0
- package/src/sync/fetchPage.test.js +84 -0
- package/src/sync/fetchRoot.js +43 -0
- package/src/sync/fetchUser.js +33 -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/createNetworkError.js +24 -0
- package/src/test/expoImage.js +42 -0
- package/src/test/expoNotifications.js +40 -0
- package/src/test/expoQuickActions.js +22 -0
- package/src/test/expoSpeechRecognition.js +31 -0
- package/src/test/expoTaskManager.js +25 -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/nativeModule.js +33 -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/src/widgets/fetchWidgetData.js +59 -0
- package/src/widgets/index.js +156 -0
- package/src/widgets/refreshWidgets.js +31 -0
- package/withExcludedNativeModules.js +93 -0
- package/dist/location.js +0 -33
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/icon-32.png +0 -0
- package/dist/public/icon-512.png +0 -0
- package/dist/public/icon.svg +0 -17
- package/dist/public/logo-dark-theme.png +0 -0
- package/dist/public/logo-light-theme.png +0 -0
- package/dist/public/logo-square-dark-theme.png +0 -0
- package/dist/public/logo-square-light-theme.png +0 -0
- package/dist/public/manifest.webmanifest +0 -16
- package/dist/shell/246.93432ff68186aae6c87b.js +0 -2
- package/dist/shell/246.93432ff68186aae6c87b.js.LICENSE.txt +0 -8
- package/dist/shell/454.b597ca21366afda000af.js +0 -2
- package/dist/shell/454.b597ca21366afda000af.js.LICENSE.txt +0 -14
- package/dist/shell/484.d645e6c09079f45ce044.js +0 -1
- package/dist/shell/706.a66931e02a549fd269bd.js +0 -2
- package/dist/shell/706.a66931e02a549fd269bd.js.LICENSE.txt +0 -8
- package/dist/shell/9.78be8c1b6c3986f404fc.js +0 -2
- package/dist/shell/9.78be8c1b6c3986f404fc.js.LICENSE.txt +0 -45
- package/dist/shell/922.43b7a3ebe22ef05b3049.js +0 -2
- package/dist/shell/922.43b7a3ebe22ef05b3049.js.LICENSE.txt +0 -14
- package/dist/shell/index.html +0 -57
- package/dist/shell/main.bde87d1433f0625faed6.js +0 -1
- package/dist/shell/runtime.1ead93159ba56c05b969.js +0 -1
|
@@ -0,0 +1,317 @@
|
|
|
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 { waitFor } from '@testing-library/react-native';
|
|
19
|
+
import { get, serializer } from '@lowdefy/helpers';
|
|
20
|
+
|
|
21
|
+
import createDevTabClient from './createDevTabClient.js';
|
|
22
|
+
import DEV_CLIENT_MARKER from './devClientMarker.js';
|
|
23
|
+
import { DEV_CLIENT_MARKER as BUNDLE_MARKER } from '../../../shellVariant.js';
|
|
24
|
+
|
|
25
|
+
// The tab channel's payloads, shared with the dev server's tests
|
|
26
|
+
// (server-dev src/routes/devInspect.test.mjs): a phone answers in a browser tab's shape.
|
|
27
|
+
const fixtures = require('../../../../servers/server-dev/test/tabChannelFixtures.json');
|
|
28
|
+
|
|
29
|
+
const apiBase = 'http://192.168.1.20:3000';
|
|
30
|
+
|
|
31
|
+
function createStream() {
|
|
32
|
+
const encoder = new TextEncoder();
|
|
33
|
+
const chunks = [];
|
|
34
|
+
let waiting = null;
|
|
35
|
+
let closed = false;
|
|
36
|
+
function deliver(result) {
|
|
37
|
+
const resolve = waiting;
|
|
38
|
+
waiting = null;
|
|
39
|
+
resolve(result);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
body: {
|
|
43
|
+
getReader: () => ({
|
|
44
|
+
read: () => {
|
|
45
|
+
if (chunks.length > 0) return Promise.resolve({ done: false, value: chunks.shift() });
|
|
46
|
+
if (closed) return Promise.resolve({ done: true });
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
waiting = resolve;
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
send(event, data) {
|
|
54
|
+
const value = encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
55
|
+
if (waiting) deliver({ done: false, value });
|
|
56
|
+
else chunks.push(value);
|
|
57
|
+
},
|
|
58
|
+
close() {
|
|
59
|
+
closed = true;
|
|
60
|
+
if (waiting) deliver({ done: true });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The app-level lowdefy object with the fixture page on screen at /tickets/7.
|
|
66
|
+
function createLowdefy() {
|
|
67
|
+
const context = {
|
|
68
|
+
...fixtures.pageContext,
|
|
69
|
+
state: { ...fixtures.pageContext.state },
|
|
70
|
+
pathParams: fixtures.page.pathParams,
|
|
71
|
+
_internal: {
|
|
72
|
+
parser: {
|
|
73
|
+
parse: ({ input }) => ({ output: get(context.state, input._state), errors: [] }),
|
|
74
|
+
},
|
|
75
|
+
State: {
|
|
76
|
+
set: (key, value) => {
|
|
77
|
+
context.state[key] = value;
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
update: jest.fn(),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
basePath: '',
|
|
85
|
+
pathMemory: new Map([['tickets/7', fixtures.page]]),
|
|
86
|
+
contexts: { [fixtures.page.instanceKey]: context },
|
|
87
|
+
inputs: { [fixtures.page.instanceKey]: fixtures.app.input },
|
|
88
|
+
lowdefyGlobal: fixtures.app.lowdefyGlobal,
|
|
89
|
+
user: fixtures.app.user,
|
|
90
|
+
_internal: {
|
|
91
|
+
globals: {
|
|
92
|
+
window: {
|
|
93
|
+
location: {
|
|
94
|
+
href: `${apiBase}/tickets/7${fixtures.app.urlQuery}`,
|
|
95
|
+
search: fixtures.app.urlQuery,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setup({ device, stream = createStream(), streamFetch, pollEvents = [] } = {}) {
|
|
104
|
+
const fetch = jest.fn(async (url) => {
|
|
105
|
+
if (url.includes('transport=poll')) {
|
|
106
|
+
return { ok: true, json: async () => ({ tabId: 'phone', events: pollEvents.splice(0) }) };
|
|
107
|
+
}
|
|
108
|
+
return { ok: true, json: async () => ({ ok: true }) };
|
|
109
|
+
});
|
|
110
|
+
const onReload = jest.fn(async () => undefined);
|
|
111
|
+
const onStep = jest.fn(async ({ step }) => ({ ok: true, ran: step }));
|
|
112
|
+
const onInject = jest.fn(({ injection }) => ({ ok: true, kind: injection.kind, pending: 1 }));
|
|
113
|
+
const client = createDevTabClient({
|
|
114
|
+
apiBase,
|
|
115
|
+
device,
|
|
116
|
+
fetch,
|
|
117
|
+
getLowdefy: createLowdefy,
|
|
118
|
+
onInject,
|
|
119
|
+
onReload,
|
|
120
|
+
onStep,
|
|
121
|
+
pollInterval: 10,
|
|
122
|
+
streamFetch: streamFetch ?? jest.fn(async () => ({ ok: true, body: stream.body })),
|
|
123
|
+
});
|
|
124
|
+
client.start();
|
|
125
|
+
return { client, fetch, onInject, onReload, onStep, stream };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function posts(fetch, path) {
|
|
129
|
+
return fetch.mock.calls
|
|
130
|
+
.filter(([url, init]) => url === `${apiBase}/api/dev-inspect${path}` && init.method === 'POST')
|
|
131
|
+
.map(([, init]) => JSON.parse(init.body));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let running;
|
|
135
|
+
afterEach(() => {
|
|
136
|
+
running?.client.stop();
|
|
137
|
+
running?.stream.close();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('the marker a release bundle is checked for is the one the client sends', () => {
|
|
141
|
+
expect(DEV_CLIENT_MARKER).toBe(BUNDLE_MARKER);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('the shell registers as a tab on the reload stream and posts the page it shows', async () => {
|
|
145
|
+
running = setup();
|
|
146
|
+
const { client, fetch, stream } = running;
|
|
147
|
+
expect(posts(fetch, '/page')).toEqual([]);
|
|
148
|
+
stream.send('tab', { tabId: 'phone' });
|
|
149
|
+
await waitFor(() =>
|
|
150
|
+
expect(posts(fetch, '/page')).toEqual([{ tabId: 'phone', ...fixtures.page }])
|
|
151
|
+
);
|
|
152
|
+
expect(client.getTabId()).toBe('phone');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('the stream is opened on /api/reload with the dev client marker', async () => {
|
|
156
|
+
const stream = createStream();
|
|
157
|
+
const streamFetch = jest.fn(async () => ({ ok: true, body: stream.body }));
|
|
158
|
+
running = setup({ stream, streamFetch });
|
|
159
|
+
await waitFor(() => expect(streamFetch).toHaveBeenCalled());
|
|
160
|
+
const [url, init] = streamFetch.mock.calls[0];
|
|
161
|
+
expect(url).toBe(`${apiBase}/api/reload?client=${DEV_CLIENT_MARKER}`);
|
|
162
|
+
expect(init.headers).toEqual({ Accept: 'text/event-stream' });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('the stream names the phone platform and app id for the device tools', async () => {
|
|
166
|
+
const stream = createStream();
|
|
167
|
+
const streamFetch = jest.fn(async () => ({ ok: true, body: stream.body }));
|
|
168
|
+
running = setup({ device: { platform: 'ios', app: 'com.lowdefy.dev' }, stream, streamFetch });
|
|
169
|
+
await waitFor(() => expect(streamFetch).toHaveBeenCalled());
|
|
170
|
+
expect(streamFetch.mock.calls[0][0]).toBe(
|
|
171
|
+
`${apiBase}/api/reload?client=${DEV_CLIENT_MARKER}&platform=ios&app=com.lowdefy.dev`
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('a reload event reloads the app', async () => {
|
|
176
|
+
running = setup();
|
|
177
|
+
running.stream.send('reload', {});
|
|
178
|
+
await waitFor(() => expect(running.onReload).toHaveBeenCalledTimes(1));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('an inspect-request is answered with the payload a browser tab posts', async () => {
|
|
182
|
+
running = setup();
|
|
183
|
+
running.stream.send(fixtures.inspectRequest.event, {
|
|
184
|
+
requestId: 'r-inspect',
|
|
185
|
+
...fixtures.page,
|
|
186
|
+
...fixtures.inspectRequest.payload,
|
|
187
|
+
});
|
|
188
|
+
await waitFor(() => expect(posts(running.fetch, '')).toHaveLength(1));
|
|
189
|
+
const [answer] = posts(running.fetch, '');
|
|
190
|
+
expect(answer.requestId).toBe('r-inspect');
|
|
191
|
+
expect(serializer.deserializeFromString(answer.result)).toEqual(fixtures.inspectResult);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('an eval-request is answered with the payload a browser tab posts', async () => {
|
|
195
|
+
running = setup();
|
|
196
|
+
running.stream.send(fixtures.evalRequest.event, {
|
|
197
|
+
requestId: 'r-eval',
|
|
198
|
+
...fixtures.page,
|
|
199
|
+
...fixtures.evalRequest.payload,
|
|
200
|
+
});
|
|
201
|
+
await waitFor(() => expect(posts(running.fetch, '')).toHaveLength(1));
|
|
202
|
+
const [answer] = posts(running.fetch, '');
|
|
203
|
+
expect(answer.requestId).toBe('r-eval');
|
|
204
|
+
expect({
|
|
205
|
+
value: serializer.deserializeFromString(answer.result.value),
|
|
206
|
+
errors: answer.result.errors,
|
|
207
|
+
}).toEqual(fixtures.evalResult);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('a request for a page with no live context is answered with an error', async () => {
|
|
211
|
+
running = setup();
|
|
212
|
+
running.stream.send('inspect-request', {
|
|
213
|
+
requestId: 'r-gone',
|
|
214
|
+
pageId: 'home',
|
|
215
|
+
pathParams: {},
|
|
216
|
+
instanceKey: 'page:home',
|
|
217
|
+
});
|
|
218
|
+
await waitFor(() => expect(posts(running.fetch, '')).toHaveLength(1));
|
|
219
|
+
expect(posts(running.fetch, '')[0]).toEqual({
|
|
220
|
+
requestId: 'r-gone',
|
|
221
|
+
result: { error: 'No live context for page "home".' },
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('a step-request runs the journey step and posts its result', async () => {
|
|
226
|
+
running = setup();
|
|
227
|
+
running.stream.send('step-request', {
|
|
228
|
+
requestId: 'r-step',
|
|
229
|
+
...fixtures.page,
|
|
230
|
+
step: { click: 'save' },
|
|
231
|
+
});
|
|
232
|
+
await waitFor(() => expect(posts(running.fetch, '')).toHaveLength(1));
|
|
233
|
+
expect(running.onStep).toHaveBeenCalledWith({
|
|
234
|
+
step: { click: 'save' },
|
|
235
|
+
page: fixtures.page,
|
|
236
|
+
});
|
|
237
|
+
expect(posts(running.fetch, '')[0]).toEqual({
|
|
238
|
+
requestId: 'r-step',
|
|
239
|
+
result: { ok: true, ran: { click: 'save' } },
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('a failed step is answered with its error', async () => {
|
|
244
|
+
running = setup();
|
|
245
|
+
running.onStep.mockImplementation(async () => {
|
|
246
|
+
throw new Error('Block "save" is not on page "ticket".');
|
|
247
|
+
});
|
|
248
|
+
running.stream.send('step-request', { requestId: 'r-fail', step: { click: 'save' } });
|
|
249
|
+
await waitFor(() => expect(posts(running.fetch, '')).toHaveLength(1));
|
|
250
|
+
expect(posts(running.fetch, '')[0].result).toEqual({
|
|
251
|
+
error: 'Block "save" is not on page "ticket".',
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('a failed step answers with the error class and its cause', async () => {
|
|
256
|
+
running = setup();
|
|
257
|
+
running.onStep.mockImplementation(async () => {
|
|
258
|
+
const error = new Error('Action "save" failed.', {
|
|
259
|
+
cause: new TypeError("Cannot read properties of undefined (reading 'id')"),
|
|
260
|
+
});
|
|
261
|
+
error.name = 'ActionError';
|
|
262
|
+
throw error;
|
|
263
|
+
});
|
|
264
|
+
running.stream.send('step-request', { requestId: 'r-class', step: { click: 'save' } });
|
|
265
|
+
await waitFor(() => expect(posts(running.fetch, '')).toHaveLength(1));
|
|
266
|
+
expect(posts(running.fetch, '')[0].result).toEqual({
|
|
267
|
+
error:
|
|
268
|
+
'[ActionError] Action "save" failed. Caused by: [TypeError] Cannot read properties of undefined (reading \'id\')',
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('an inject request fakes the next device result and posts what was queued', async () => {
|
|
273
|
+
running = setup();
|
|
274
|
+
running.stream.send('inject', {
|
|
275
|
+
requestId: 'r-inject',
|
|
276
|
+
inject: { kind: 'scan', value: { data: 'JOB-42', type: 'qr' } },
|
|
277
|
+
});
|
|
278
|
+
await waitFor(() => expect(posts(running.fetch, '')).toHaveLength(1));
|
|
279
|
+
expect(running.onInject).toHaveBeenCalledWith({
|
|
280
|
+
injection: { kind: 'scan', value: { data: 'JOB-42', type: 'qr' } },
|
|
281
|
+
});
|
|
282
|
+
expect(posts(running.fetch, '')[0]).toEqual({
|
|
283
|
+
requestId: 'r-inject',
|
|
284
|
+
result: { ok: true, kind: 'scan', pending: 1 },
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('a refused injection is answered with why', async () => {
|
|
289
|
+
running = setup();
|
|
290
|
+
running.onInject.mockImplementation(() => {
|
|
291
|
+
throw new Error('A scan injection needs { data }. Received "JOB-42".');
|
|
292
|
+
});
|
|
293
|
+
running.stream.send('inject', { requestId: 'r-bad', inject: { kind: 'scan', value: 'JOB-42' } });
|
|
294
|
+
await waitFor(() => expect(posts(running.fetch, '')).toHaveLength(1));
|
|
295
|
+
expect(posts(running.fetch, '')[0].result).toEqual({
|
|
296
|
+
error: 'A scan injection needs { data }. Received "JOB-42".',
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('without a readable stream the shell polls every interval, as a passive caller', async () => {
|
|
301
|
+
const pollEvents = [
|
|
302
|
+
{ event: 'tab', data: JSON.stringify({ tabId: 'phone' }) },
|
|
303
|
+
{ event: 'reload', data: '{}' },
|
|
304
|
+
];
|
|
305
|
+
running = setup({
|
|
306
|
+
pollEvents,
|
|
307
|
+
streamFetch: jest.fn(async () => ({ ok: true, body: null })),
|
|
308
|
+
});
|
|
309
|
+
await waitFor(() => expect(running.onReload).toHaveBeenCalledTimes(1));
|
|
310
|
+
await waitFor(() => expect(posts(running.fetch, '/page')).toHaveLength(1));
|
|
311
|
+
const polls = running.fetch.mock.calls.filter(([url]) => url.includes('transport=poll'));
|
|
312
|
+
expect(polls[0][0]).toBe(`${apiBase}/api/reload?transport=poll&client=${DEV_CLIENT_MARKER}`);
|
|
313
|
+
expect(polls[0][1].headers).toEqual({ 'x-lowdefy-passive': '1' });
|
|
314
|
+
await waitFor(() =>
|
|
315
|
+
expect(running.fetch.mock.calls.some(([url]) => url.endsWith('&tabId=phone'))).toBe(true)
|
|
316
|
+
);
|
|
317
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
function describeOne(error) {
|
|
18
|
+
if (error?.name === undefined || error.name === 'Error') {
|
|
19
|
+
return String(error?.message ?? error);
|
|
20
|
+
}
|
|
21
|
+
return `[${error.name}] ${error.message}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The error a failed inspect, eval or step answers the agent with: a string, as the dev server
|
|
25
|
+
// reads it, that keeps the class ([OperatorError] ..., as the dev terminal shows it) and the cause,
|
|
26
|
+
// so the agent can tell a config fault from an expected refusal or a shell bug.
|
|
27
|
+
function describeTabError(error) {
|
|
28
|
+
const description = describeOne(error);
|
|
29
|
+
if (error?.cause instanceof Error) {
|
|
30
|
+
return `${description} Caused by: ${describeOne(error.cause)}`;
|
|
31
|
+
}
|
|
32
|
+
return description;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default describeTabError;
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
// Sent with every connection the dev-tab client opens, so the dev server knows a shell's tab from a
|
|
18
|
+
// browser's, and so a release bundle that holds it fails scripts/checkReleaseBundle.mjs. Keep it in
|
|
19
|
+
// step with shellVariant.js.
|
|
20
|
+
const DEV_CLIENT_MARKER = 'lowdefy-dev-tab-client';
|
|
21
|
+
|
|
22
|
+
export default DEV_CLIENT_MARKER;
|
|
@@ -0,0 +1,23 @@
|
|
|
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 DEV_CLIENT_MARKER from './devClientMarker.js';
|
|
18
|
+
|
|
19
|
+
const { DEV_CLIENT_MARKER: RELEASE_CHECK_MARKER } = require('../../../shellVariant.js');
|
|
20
|
+
|
|
21
|
+
test('the dev-tab client marker matches the one the release bundle check looks for', () => {
|
|
22
|
+
expect(DEV_CLIENT_MARKER).toBe(RELEASE_CHECK_MARKER);
|
|
23
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
|
|
19
|
+
// `page "ticket"`, or `page "ticket" (tickets/s/1)` for an instance of a page with placeholders,
|
|
20
|
+
// whose instance key holds the path its values build. Worded as the browser tab words it. A
|
|
21
|
+
// screen's key ends in `@{instanceId}`, its route instance, which is left out.
|
|
22
|
+
function describePage({ pageId, instanceKey }) {
|
|
23
|
+
const pageKey = instanceKey.split('@')[0];
|
|
24
|
+
const hashIndex = pageKey.indexOf('#');
|
|
25
|
+
if (hashIndex === -1) {
|
|
26
|
+
return `page "${pageId}"`;
|
|
27
|
+
}
|
|
28
|
+
return `page "${pageId}" (${pageKey.slice(hashIndex + 1)})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The engine context of a page instance: `{ context }`, or `{ error }` as a browser tab answers it.
|
|
32
|
+
function findPageContext({ lowdefy, page }) {
|
|
33
|
+
if (type.isNone(page)) {
|
|
34
|
+
return { error: 'No page has rendered in this tab yet.' };
|
|
35
|
+
}
|
|
36
|
+
const context = lowdefy?.contexts?.[page.instanceKey];
|
|
37
|
+
if (type.isNone(context)) {
|
|
38
|
+
return { error: `No live context for ${describePage(page)}.` };
|
|
39
|
+
}
|
|
40
|
+
return { context };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default findPageContext;
|
|
@@ -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;
|