From d935d814359a04cffbdf2d751ed46f3b6c64039a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 20 Aug 2026 00:32:48 +0000 Subject: [PATCH] chore: publish Quickstarts for SDK version 5.0.3 --- .env.sample | 9 +- .gitattributes | 17 + .github/CODEOWNERS | 27 +- .../generate-browserstack-names/action.yml | 56 - .../actions/seed-ditto-document/action.yml | 78 - .github/assets/ditto-portal.png | Bin 299353 -> 0 bytes .github/browserstack-devices.yml | 110 - .github/scripts/browserstack-test.py | 277 - .github/workflows/android-java-ci.yml | 343 - .github/workflows/android-kotlin-ci.yml | 361 - .github/workflows/cpp-tui-ci.yml | 145 - .github/workflows/dotnet-maui-ci.yml | 450 - .github/workflows/dotnet-tui-ci.yml | 141 - .github/workflows/dotnet-winforms-ci.yml | 134 - .github/workflows/flutter-ci.yml | 948 -- .github/workflows/java-server-ci.yml | 349 - .github/workflows/javascript-tui-ci.yml | 142 - .github/workflows/javascript-web-ci.yml | 257 - .github/workflows/kotlin-multiplatform-ci.yml | 557 - .github/workflows/publisher-authorization.yml | 28 + .github/workflows/react-native-ci.yml | 649 - .github/workflows/react-native-expo-ci.yml | 654 - .github/workflows/rust-tui-ci.yml | 165 - .github/workflows/security-update-claude.yml | 51 - .github/workflows/swift-ci.yml | 563 - .github/workflows/test-build-info-action.yml | 39 - .github/workflows/update-sdk-versions.yml | 129 - CLAUDE.md | 269 - LICENSE | 2 +- README.md | 20 +- android-java/README.md | 24 +- android-java/app/build.gradle.kts | 28 +- android-java/app/src/main/AndroidManifest.xml | 1 + .../com/example/dittotasks/DittoHelper.kt | 75 - .../com/example/dittotasks/DittoManager.kt | 75 + .../com/example/dittotasks/MainActivity.java | 206 +- .../example/dittotasks/TasksApplication.java | 29 + .../com/example/dittotasks/TasksRepository.kt | 123 + .../app/src/main/res/layout/activity_main.xml | 4 +- .../app/src/main/res/values/strings.xml | 4 +- android-java/gradle/libs.versions.toml | 2 +- .../QuickStartTasks/app/build.gradle.kts | 12 +- .../ditto/quickstart/tasks/DittoHandler.kt | 20 - .../ditto/quickstart/tasks/DittoManager.kt | 78 + .../quickstart/tasks/TasksApplication.kt | 34 +- .../quickstart/tasks/data/TasksRepository.kt | 98 + .../tasks/edit/EditScreenViewModel.kt | 61 +- .../quickstart/tasks/list/TasksListScreen.kt | 8 +- .../tasks/list/TasksListScreenViewModel.kt | 136 +- .../QuickStartTasks/gradle/libs.versions.toml | 2 +- android-kotlin/README.md | 16 +- .../js-web-ditto-screenshot.png | Bin cpp-tui/README.md | 6 +- cpp-tui/taskscpp/CMakeLists.txt | 3 +- cpp-tui/taskscpp/Makefile | 2 +- cpp-tui/taskscpp/src/ditto_manager.cpp | 124 + cpp-tui/taskscpp/src/ditto_manager.h | 54 + cpp-tui/taskscpp/src/main.cpp | 144 +- cpp-tui/taskscpp/src/tasks_log.cpp | 30 +- cpp-tui/taskscpp/src/tasks_log.h | 3 - cpp-tui/taskscpp/src/tasks_peer.cpp | 455 - cpp-tui/taskscpp/src/tasks_peer.h | 116 - cpp-tui/taskscpp/src/tasks_repository.cpp | 254 + cpp-tui/taskscpp/src/tasks_repository.h | 85 + cpp-tui/taskscpp/src/tasks_tui.cpp | 54 +- cpp-tui/taskscpp/src/tasks_tui.h | 10 +- cpp-tui/taskscpp/tests/integration_test.cpp | 143 +- cpp-tui/taskscpp/tests/run_tests.sh | 4 +- dotnet-maui/DittoMauiTasksApp/DittoManager.cs | 130 + .../DittoMauiTasksApp.csproj | 134 +- dotnet-maui/DittoMauiTasksApp/MauiProgram.cs | 232 +- .../DittoMauiTasksApp/Model/DittoTask.cs | 25 - .../DittoMauiTasksApp/Model/TaskModel.cs | 34 + .../Platforms/MacCatalyst/AppDelegate.cs | 2 +- .../Platforms/MacCatalyst/Program.cs | 2 +- .../DittoMauiTasksApp/TasksRepository.cs | 151 + .../DittoMauiTasksApp/Utils/PopupService.cs | 28 +- .../ViewModels/TasksPageviewModel.cs | 678 +- .../DittoMauiTasksApp/Views/TasksPage.xaml | 186 +- .../DittoMauiTasksApp/Views/TasksPage.xaml.cs | 62 +- dotnet-maui/README.md | 60 +- .../UITests.Android/AndroidTaskSearchTests.cs | 172 +- dotnet-maui/UITests.Android/Program.cs | 2 +- dotnet-maui/UITests.Shared/BaseTest.cs | 246 +- dotnet-maui/UITests.Shared/TaskSearchTests.cs | 174 +- dotnet-maui/UITests.iOS/Program.cs | 2 +- dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs | 254 +- .../DittoDotNetTasksConsole.Tests.csproj | 2 +- .../IntegrationTests.cs | 22 +- .../DittoDotNetTasksConsole.csproj | 2 +- .../DittoDotNetTasksConsole/DittoManager.cs | 114 + dotnet-tui/DittoDotNetTasksConsole/Program.cs | 16 +- .../{ToDoTask.cs => TaskModel.cs} | 4 +- .../DittoDotNetTasksConsole/TasksPeer.cs | 284 - .../TasksRepository.cs | 148 + .../DittoDotNetTasksConsole/TasksWindow.cs | 72 +- dotnet-tui/README.md | 6 +- dotnet-winforms-net48/.env.sample | 14 +- dotnet-winforms-net48/App.config | 34 +- dotnet-winforms-net48/AppConfiguration.cs | 30 +- dotnet-winforms-net48/AppServices.cs | 95 + dotnet-winforms-net48/DittoManager.cs | 118 + dotnet-winforms-net48/MainForm.Designer.cs | 328 +- dotnet-winforms-net48/MainForm.cs | 513 +- dotnet-winforms-net48/MainForm.resx | 238 +- dotnet-winforms-net48/Program.cs | 136 +- .../Properties/AssemblyInfo.cs | 66 +- .../Properties/Resources.Designer.cs | 142 +- .../Properties/Resources.resx | 232 +- .../Properties/Settings.Designer.cs | 60 +- .../Properties/Settings.settings | 14 +- dotnet-winforms-net48/README.md | 6 +- .../{ToDoTask.cs => TaskModel.cs} | 58 +- .../Taskapp.WinForms.Net48.csproj | 320 +- .../Taskapp.WinForms.Net48.sln | 50 +- dotnet-winforms-net48/TasksPeer.cs | 290 - dotnet-winforms-net48/TasksPeerService.cs | 128 - dotnet-winforms-net48/TasksRepository.cs | 157 + dotnet-winforms-net48/packages.config | 40 +- .../IntegrationTest/IntegrationTest.csproj | 5 +- dotnet-winforms/IntegrationTest/Program.cs | 26 +- dotnet-winforms/README.md | 6 +- .../TasksApp/AboutForm.Designer.cs | 182 +- dotnet-winforms/TasksApp/AboutForm.cs | 15 +- dotnet-winforms/TasksApp/DittoManager.cs | 118 + dotnet-winforms/TasksApp/DittoTasksApp.csproj | 2 +- dotnet-winforms/TasksApp/MainForm.Designer.cs | 16 +- dotnet-winforms/TasksApp/MainForm.cs | 42 +- dotnet-winforms/TasksApp/Program.cs | 14 +- ...Designer.cs => TaskEditorForm.Designer.cs} | 10 +- ...oDoTaskEditorForm.cs => TaskEditorForm.cs} | 16 +- ...askEditorForm.resx => TaskEditorForm.resx} | 0 .../TasksApp/{ToDoTask.cs => TaskModel.cs} | 4 +- dotnet-winforms/TasksApp/TasksPeer.cs | 290 - dotnet-winforms/TasksApp/TasksRepository.cs | 151 + electron/README.md | 17 +- electron/package-lock.json | 1286 +- electron/package.json | 12 +- electron/src/main/ditto-manager.ts | 70 + electron/src/main/ditto.ts | 130 - electron/src/main/env.ts | 17 +- electron/src/main/index.ts | 47 +- electron/src/main/tasks-repository.ts | 78 + electron/src/renderer/App.tsx | 12 +- .../src/renderer/components/DittoInfo.tsx | 6 +- electron/src/types.ts | 2 +- electron/tailwind.config.js | 5 +- electron/tsconfig.web.json | 6 +- flutter_app/README.md | 6 +- flutter_app/android/build.gradle | 6 + flutter_app/android/gradlew.bat | 188 +- flutter_app/integration_test/app_test.dart | 29 +- flutter_app/ios/Podfile.lock | 12 +- flutter_app/lib/dialog.dart | 20 +- flutter_app/lib/ditto_manager.dart | 86 + flutter_app/lib/dql_builder.dart | 110 - flutter_app/lib/main.dart | 169 +- flutter_app/lib/tasks_repository.dart | 79 + flutter_app/macos/Podfile.lock | 12 +- .../macos/Runner.xcodeproj/project.pbxproj | 6 +- flutter_app/pubspec.lock | 6 +- flutter_app/pubspec.yaml | 14 +- flutter_app/test/dql_safety_test.dart | 71 + flutter_app/test/widget_test.dart | 16 +- flutter_app/windows/CMakeLists.txt | 8 + global.json | 7 + go-tui/README.md | 16 +- go-tui/ditto_manager.go | 85 + go-tui/main.go | 168 +- go-tui/tasks_repository.go | 154 + java-server/README.md | 6 +- java-server/build.gradle.kts | 10 +- java-server/java-spring-maven/pom.xml | 4 +- java-server/settings.gradle.kts | 2 +- .../controller/DittoConfigRestController.java | 41 +- .../controller/TaskContentController.java | 10 +- .../controller/TaskRestController.java | 28 +- .../quickstart/service/DittoManager.java | 130 + .../quickstart/service/DittoService.java | 165 - ...oTaskService.java => TasksRepository.java} | 64 +- .../templates/fragments/taskList.html | 2 +- .../src/main/resources/templates/index.html | 21 +- .../TaskVisibilityIntegrationTest.java | 48 +- javascript-tui/.prettierignore | 1 + javascript-tui/README.md | 26 +- javascript-tui/package-lock.json | 13838 ++++++++-------- javascript-tui/package.json | 130 +- javascript-tui/prettier.config.cjs | 7 + javascript-tui/prettier.config.js | 9 - javascript-tui/source/app.js | 576 +- javascript-tui/source/cli.js | 136 +- javascript-tui/source/ditto-manager.js | 92 + javascript-tui/source/tasks-repository.js | 82 + javascript-tui/tests/integration_test.js | 151 +- javascript-web/README.md | 18 +- javascript-web/package-lock.json | 1485 +- javascript-web/package.json | 6 +- javascript-web/src/App.tsx | 217 +- javascript-web/src/components/DittoInfo.tsx | 6 +- javascript-web/src/hooks/useDitto.ts | 123 + javascript-web/src/hooks/useTasks.ts | 130 + .../.run/composeApp [desktop].run.xml | 4 +- kotlin-multiplatform/README.md | 135 +- .../composeApp/build.gradle.kts | 145 +- .../quickstart/ditto/DittoManager.android.kt | 2 - .../res/xml/network_security_config.xml | 2 +- .../ditto/quickstart/data/DittoCredentials.kt | 2 +- .../ditto/quickstart/data/dto/AddTaskDto.kt | 9 - .../data/repository/DittoTaskRepository.kt | 150 - .../data/repository/TaskRepository.kt | 6 +- .../data/repository/TasksRepository.kt | 130 + .../data/screenstate/MainScreenState.kt | 4 +- .../ditto/quickstart/di/RepositoryModule.kt | 4 +- .../ditto/quickstart/ditto/DittoManager.kt | 67 +- .../com/ditto/quickstart/ui/MainScreen.kt | 2 +- .../quickstart/ui/MainScreenViewModel.kt | 4 +- .../quickstart/ui/TaskListScreenViewModel.kt | 2 +- .../ditto/quickstart/ui/components/TopBar.kt | 6 +- .../usecases/SetSyncStatusUseCase.kt | 6 +- kotlin-multiplatform/detekt.yml | 133 +- .../iosApp/iosApp.xcodeproj/project.pbxproj | 2 +- ...1-app-launch-and-seeded-tasks-android.yaml | 12 +- .../01-app-launch-and-seeded-tasks-ios.yaml | 14 +- react-native-expo/App.tsx | 302 +- react-native-expo/README.md | 2 +- react-native-expo/android/gradle.properties | 9 +- react-native-expo/babel.config.js | 8 +- react-native-expo/components/DittoInfo.tsx | 10 +- .../components/EditTaskModal.tsx | 27 +- react-native-expo/components/Fab.tsx | 7 +- react-native-expo/components/NewTaskModal.tsx | 22 +- react-native-expo/components/TaskDone.tsx | 6 +- react-native-expo/eslint.config.js | 6 +- react-native-expo/hooks/useDitto.ts | 141 + react-native-expo/hooks/useTasks.ts | 96 + react-native-expo/main.tsx | 4 +- react-native-expo/metro.config.js | 2 +- react-native-expo/package-lock.json | 52 +- react-native-expo/package.json | 16 +- react-native-expo/scripts/reset-project.js | 42 +- react-native-expo/types/env.d.ts | 8 +- react-native-expo/yarn.lock | 69 +- ...1-app-launch-and-seeded-tasks-android.yaml | 10 +- .../01-app-launch-and-seeded-tasks-ios.yaml | 14 +- react-native/App.tsx | 197 +- react-native/Gemfile | 2 +- react-native/Gemfile.lock | 4 +- react-native/README.md | 6 +- react-native/__mocks__/@env.js | 6 +- .../android/app/src/main/AndroidManifest.xml | 5 + react-native/android/gradle.properties | 9 +- react-native/components/DittoInfo.tsx | 4 +- react-native/components/DittoSync.tsx | 6 +- react-native/components/EditTaskModal.tsx | 6 +- react-native/components/NewTaskModal.tsx | 4 +- react-native/components/TaskDone.tsx | 4 +- react-native/hooks/useDitto.ts | 138 + react-native/hooks/useTasks.ts | 99 + react-native/ios/Podfile | 15 + react-native/jest.setup.js | 14 +- react-native/package-lock.json | 30 +- react-native/package.json | 10 +- react-native/types/env.d.ts | 6 +- react-native/yarn.lock | 26 +- rust-tui/.cargo/config.toml | 10 + rust-tui/Cargo.lock | 27 +- rust-tui/Cargo.toml | 2 +- rust-tui/README.md | 12 +- rust-tui/clippy.toml | 11 + rust-tui/src/bin/integration_test.rs | 116 +- rust-tui/src/bin/main.rs | 153 +- rust-tui/src/ditto_manager.rs | 114 + rust-tui/src/lib.rs | 9 +- rust-tui/src/tasks_repository.rs | 148 + rust-tui/src/term.rs | 5 +- rust-tui/src/tui/mod.rs | 17 +- rust-tui/src/tui/todolist.rs | 177 +- scripts/README.md | 2 - scripts/bump-versions.js | 412 - swift/.swiftlint.yml | 2 +- swift/README.md | 6 +- swift/Tasks.xcodeproj/project.pbxproj | 10 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- swift/Tasks/DittoManager.swift | 52 +- swift/Tasks/EditScreen.swift | 10 +- swift/Tasks/TasksListScreen.swift | 271 +- swift/Tasks/TasksRepository.swift | 146 + swift/TasksUITests/TasksUITests.swift | 4 +- swift/buildEnv.sh | 27 +- 289 files changed, 17480 insertions(+), 23775 deletions(-) create mode 100644 .gitattributes delete mode 100644 .github/actions/generate-browserstack-names/action.yml delete mode 100644 .github/actions/seed-ditto-document/action.yml delete mode 100644 .github/assets/ditto-portal.png delete mode 100644 .github/browserstack-devices.yml delete mode 100644 .github/scripts/browserstack-test.py delete mode 100644 .github/workflows/android-java-ci.yml delete mode 100644 .github/workflows/android-kotlin-ci.yml delete mode 100644 .github/workflows/cpp-tui-ci.yml delete mode 100644 .github/workflows/dotnet-maui-ci.yml delete mode 100644 .github/workflows/dotnet-tui-ci.yml delete mode 100644 .github/workflows/dotnet-winforms-ci.yml delete mode 100644 .github/workflows/flutter-ci.yml delete mode 100644 .github/workflows/java-server-ci.yml delete mode 100644 .github/workflows/javascript-tui-ci.yml delete mode 100644 .github/workflows/javascript-web-ci.yml delete mode 100644 .github/workflows/kotlin-multiplatform-ci.yml create mode 100644 .github/workflows/publisher-authorization.yml delete mode 100644 .github/workflows/react-native-ci.yml delete mode 100644 .github/workflows/react-native-expo-ci.yml delete mode 100644 .github/workflows/rust-tui-ci.yml delete mode 100644 .github/workflows/security-update-claude.yml delete mode 100644 .github/workflows/swift-ci.yml delete mode 100644 .github/workflows/test-build-info-action.yml delete mode 100644 .github/workflows/update-sdk-versions.yml delete mode 100644 CLAUDE.md delete mode 100644 android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt create mode 100644 android-java/app/src/main/java/com/example/dittotasks/DittoManager.kt create mode 100644 android-java/app/src/main/java/com/example/dittotasks/TasksApplication.java create mode 100644 android-java/app/src/main/java/com/example/dittotasks/TasksRepository.kt delete mode 100644 android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoHandler.kt create mode 100644 android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoManager.kt create mode 100644 android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/data/TasksRepository.kt rename {.github/assets => assets}/js-web-ditto-screenshot.png (100%) create mode 100644 cpp-tui/taskscpp/src/ditto_manager.cpp create mode 100644 cpp-tui/taskscpp/src/ditto_manager.h delete mode 100644 cpp-tui/taskscpp/src/tasks_peer.cpp delete mode 100644 cpp-tui/taskscpp/src/tasks_peer.h create mode 100644 cpp-tui/taskscpp/src/tasks_repository.cpp create mode 100644 cpp-tui/taskscpp/src/tasks_repository.h create mode 100644 dotnet-maui/DittoMauiTasksApp/DittoManager.cs delete mode 100644 dotnet-maui/DittoMauiTasksApp/Model/DittoTask.cs create mode 100644 dotnet-maui/DittoMauiTasksApp/Model/TaskModel.cs create mode 100644 dotnet-maui/DittoMauiTasksApp/TasksRepository.cs create mode 100644 dotnet-tui/DittoDotNetTasksConsole/DittoManager.cs rename dotnet-tui/DittoDotNetTasksConsole/{ToDoTask.cs => TaskModel.cs} (85%) delete mode 100644 dotnet-tui/DittoDotNetTasksConsole/TasksPeer.cs create mode 100644 dotnet-tui/DittoDotNetTasksConsole/TasksRepository.cs create mode 100644 dotnet-winforms-net48/AppServices.cs create mode 100644 dotnet-winforms-net48/DittoManager.cs rename dotnet-winforms-net48/{ToDoTask.cs => TaskModel.cs} (83%) delete mode 100644 dotnet-winforms-net48/TasksPeer.cs delete mode 100644 dotnet-winforms-net48/TasksPeerService.cs create mode 100644 dotnet-winforms-net48/TasksRepository.cs create mode 100644 dotnet-winforms/TasksApp/DittoManager.cs rename dotnet-winforms/TasksApp/{ToDoTaskEditorForm.Designer.cs => TaskEditorForm.Designer.cs} (96%) rename dotnet-winforms/TasksApp/{ToDoTaskEditorForm.cs => TaskEditorForm.cs} (69%) rename dotnet-winforms/TasksApp/{ToDoTaskEditorForm.resx => TaskEditorForm.resx} (100%) rename dotnet-winforms/TasksApp/{ToDoTask.cs => TaskModel.cs} (86%) delete mode 100644 dotnet-winforms/TasksApp/TasksPeer.cs create mode 100644 dotnet-winforms/TasksApp/TasksRepository.cs create mode 100644 electron/src/main/ditto-manager.ts delete mode 100644 electron/src/main/ditto.ts create mode 100644 electron/src/main/tasks-repository.ts create mode 100644 flutter_app/lib/ditto_manager.dart delete mode 100644 flutter_app/lib/dql_builder.dart create mode 100644 flutter_app/lib/tasks_repository.dart create mode 100644 flutter_app/test/dql_safety_test.dart create mode 100644 global.json create mode 100644 go-tui/ditto_manager.go create mode 100644 go-tui/tasks_repository.go create mode 100644 java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoManager.java delete mode 100644 java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java rename java-server/src/main/java/com/ditto/example/spring/quickstart/service/{DittoTaskService.java => TasksRepository.java} (65%) create mode 100644 javascript-tui/prettier.config.cjs delete mode 100644 javascript-tui/prettier.config.js create mode 100644 javascript-tui/source/ditto-manager.js create mode 100644 javascript-tui/source/tasks-repository.js create mode 100644 javascript-web/src/hooks/useDitto.ts create mode 100644 javascript-web/src/hooks/useTasks.ts delete mode 100644 kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/DittoTaskRepository.kt create mode 100644 kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/TasksRepository.kt create mode 100644 react-native-expo/hooks/useDitto.ts create mode 100644 react-native-expo/hooks/useTasks.ts create mode 100644 react-native/hooks/useDitto.ts create mode 100644 react-native/hooks/useTasks.ts create mode 100644 rust-tui/.cargo/config.toml create mode 100644 rust-tui/clippy.toml create mode 100644 rust-tui/src/ditto_manager.rs create mode 100644 rust-tui/src/tasks_repository.rs delete mode 100644 scripts/README.md delete mode 100755 scripts/bump-versions.js create mode 100644 swift/Tasks/TasksRepository.swift diff --git a/.env.sample b/.env.sample index aab8aa552..d65021c1c 100644 --- a/.env.sample +++ b/.env.sample @@ -1,8 +1,7 @@ #!/usr/bin/env bash # Copy this file from ".env.sample" to ".env", then fill in these values -# A Ditto AppID and Playground token can be obtained from https://portal.ditto.live -DITTO_APP_ID="" -DITTO_PLAYGROUND_TOKEN="" -DITTO_AUTH_URL="" -DITTO_WEBSOCKET_URL="" \ No newline at end of file +# A Ditto Database ID and development token can be obtained from https://portal.ditto.live +DITTO_DATABASE_ID="" +DITTO_DEVELOPMENT_TOKEN="" +DITTO_SERVER_URL="" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f20731ebb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# Line-ending policy for the published Quickstart apps. +# +# The publish process exports this subtree with `git archive`, which resolves +# .gitattributes relative to THIS directory. Without a .gitattributes here, the +# parent EOL rules are not applied to the exported output, so every file is +# exported with LF -- dropping the CRLF that Windows batch files require. These +# rules force LF everywhere (a blanket `eol=lf`) plus an explicit `*.cmd` rule, +# so the published apps (and this subtree) get deterministic, convention-correct +# line endings. + +# Normalize all text to LF in the repository and on checkout/export +# (matches the repo-root .editorconfig `end_of_line = lf`). +* text=auto eol=lf + +# Windows batch/command scripts require CRLF for cmd.exe. +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 52730de80..dfb910bb6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,26 +1,5 @@ +# CODEOWNERS for the public getditto/quickstart mirror. # -# .github/CODEOWNERS -# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -# - -# Global default owners +# This repository is a read-only mirror published by Ditto's SDK release +# process. The SDK engineers team owns the entire published mirror. * @getditto/sdk-engineers - -# SDK-specific owners -/android-java/ @phatblat @busec0 -/android-kotlin/ @phatblat @busec0 -/cpp-tui/ @kristopherjohnson -/dotnet-maui/ @busec0 @kristopherjohnson -/dotnet-tui/ @busec0 @kristopherjohnson -/dotnet-winforms/ @busec0 @kristopherjohnson -/flutter_app/ @alycda -/go-tui/ @kristopherjohnson @dsharp-pivotal -/java-spring/ @phatblat @busec0 -/java-server/ @phatblat @busec0 -/javascript-tui/ @nickrobinson -/javascript-web/ @nickrobinson -/kotlin-multiplatform/ @phatblat @busec0 -/react-native/ @kristopherjohnson -/react-native-expo/ @kristopherjohnson -/rust-tui/ @kristopherjohnson -/swift/ @SausCode @busec0 diff --git a/.github/actions/generate-browserstack-names/action.yml b/.github/actions/generate-browserstack-names/action.yml deleted file mode 100644 index 6adf4f62e..000000000 --- a/.github/actions/generate-browserstack-names/action.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: 'Generate BrowserStack Names' -description: 'Auto-generates BrowserStack project name from workflow context' -inputs: - platform-suffix: - description: 'Optional platform suffix to append to build name (e.g., " (Android)", " (iOS)")' - required: false - default: '' -outputs: - project-name: - description: 'BrowserStack project name (auto-derived from workflow)' - value: ${{ steps.generate.outputs.project-name }} - build-name: - description: 'BrowserStack build name (currently returns empty - FIXME)' - value: ${{ steps.generate.outputs.build-name }} - -runs: - using: 'composite' - steps: - - name: Generate BrowserStack names - id: generate - shell: bash - run: | - # Auto-derive project name from workflow file name - # Pattern: -ci.yml -> quickstart - - WORKFLOW_FILE="${{ github.workflow }}" - - echo "šŸ” Workflow: $WORKFLOW_FILE" - - # Extract platform name from workflow name and convert to title case - # Examples: - # "JavaScript Web CI" -> "JavaScript Web" - # "Android Kotlin CI" -> "Android Kotlin" - # "React Native Expo CI" -> "React Native Expo" - PLATFORM_NAME=$(echo "$WORKFLOW_FILE" | sed 's/ CI$//') - - PROJECT_NAME="quickstart - $PLATFORM_NAME" - - # FIXME: Build name generation is currently broken and doesn't work properly - # For now, we return an empty string. This needs to be fixed in a future PR. - # The build name should ideally include PR context and commit info, but the - # current implementation using GitHub CLI and event context is unreliable. - # - # TODO: Implement proper build name generation that: - # 1. Reliably fetches PR information - # 2. Sanitizes strings for BrowserStack requirements - # 3. Handles both PR and non-PR contexts - # 4. Includes platform suffix when provided - BUILD_NAME="" - - # Output values - echo "project-name=$PROJECT_NAME" >> $GITHUB_OUTPUT - echo "build-name=$BUILD_NAME" >> $GITHUB_OUTPUT - - # Debug output - echo "šŸ“¦ Project: $PROJECT_NAME" - echo "šŸ—ļø Build: $BUILD_NAME (empty - FIXME)" diff --git a/.github/actions/seed-ditto-document/action.yml b/.github/actions/seed-ditto-document/action.yml deleted file mode 100644 index cad09b91d..000000000 --- a/.github/actions/seed-ditto-document/action.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: 'Seed Ditto Test Document' -description: 'Creates a test document in Ditto Cloud with inverted timestamp for CI testing' -author: 'Ditto' - -inputs: - ditto-api-key: - description: 'Ditto API key for authentication' - required: true - ditto-api-url: - description: 'Ditto API URL (e.g., cloud.ditto.live)' - required: true - app-name: - description: 'Name of the app/platform being tested (e.g., android-kotlin, swift, etc.)' - required: true - -outputs: - document-id: - description: 'The generated document ID' - value: ${{ steps.seed.outputs.document_id }} - document-title: - description: 'The generated document title (same as ID)' - value: ${{ steps.seed.outputs.document_title }} - inverted-timestamp: - description: 'The inverted timestamp used for ordering' - value: ${{ steps.seed.outputs.inverted_timestamp }} - -runs: - using: 'composite' - steps: - - name: Generate and insert test document - id: seed - shell: bash - run: | - # Generate inverted timestamp for top position in list - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - - # Create unique document ID and title - DOC_ID="${INVERTED_TIMESTAMP}_${{ inputs.app-name }}_ci_test_${{ github.run_id }}_${{ github.run_number }}" - DOC_TITLE="$DOC_ID" - - echo "šŸ“ Seeding test document to Ditto Cloud" - echo "šŸ“ App: ${{ inputs.app-name }}" - echo "šŸ“ ID: ${DOC_ID}" - echo "šŸ“ Timestamp: ${TIMESTAMP} → Inverted: ${INVERTED_TIMESTAMP}" - - # Insert document using Ditto API v4 - RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ inputs.ditto-api-key }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${DOC_ID}\", - \"title\": \"${DOC_TITLE}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ inputs.ditto-api-url }}/api/v4/store/execute") - - # Extract HTTP status code and response body (portable version) - HTTP_CODE=$(echo "$RESPONSE" | tail -n1) - BODY=$(echo "$RESPONSE" | sed '$d') - - # Check if insertion was successful - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - echo "āœ… Successfully inserted test document" - echo "document_id=${DOC_ID}" >> $GITHUB_OUTPUT - echo "document_title=${DOC_TITLE}" >> $GITHUB_OUTPUT - echo "inverted_timestamp=${INVERTED_TIMESTAMP}" >> $GITHUB_OUTPUT - else - echo "āŒ Failed to insert document. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi diff --git a/.github/assets/ditto-portal.png b/.github/assets/ditto-portal.png deleted file mode 100644 index fabec988510ba1a3827b6c9825d8958ac222dab9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 299353 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%0D9CQkz0DJdS6jkU)SYizOZD&pltoCbIxFS6HD%x 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≶ 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�s#oblC#sxZl11(AR6?sg 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 diff --git a/.github/browserstack-devices.yml b/.github/browserstack-devices.yml deleted file mode 100644 index 98b14892f..000000000 --- a/.github/browserstack-devices.yml +++ /dev/null @@ -1,110 +0,0 @@ ---- -# BrowserStack Device Configuration -# -# Centralized configuration for all BrowserStack testing across CI workflows. -# Each SDK/app has its own configuration section below. -# -# Device Selection Criteria: -# - Coverage: Test across multiple OS versions (latest, current-1, current-2) -# - Market share: Focus on popular devices (Pixel, Samsung Galaxy, iPhone) -# - OS baseline: Ensure minimum supported OS versions are tested -# -# References: -# - Mobile devices: https://www.browserstack.com/docs/app-automate/api-reference/appium/devices -# - Desktop browsers: https://www.browserstack.com/docs/automate/api-reference/selenium/list-of-browsers-and-platforms - -# Android Java QuickStart -android-java: - devices: - - "Google Pixel 8-14.0" # Latest Android 14 - - "Samsung Galaxy S23-13.0" # Popular flagship, Android 13 - - "Google Pixel 6-12.0" # Android 12 support - - "OnePlus 9-11.0" # Android 11 minimum baseline - -# Android Kotlin QuickStart -android-kotlin: - devices: - - "Google Pixel 8-14.0" # Latest Android 14 - - "Samsung Galaxy S23-13.0" # Popular flagship, Android 13 - - "Google Pixel 6-12.0" # Android 12 support - - "OnePlus 9-11.0" # Android 11 minimum baseline - -# Flutter QuickStart (multi-platform) -flutter: - android: - devices: - - "Google Pixel 7-13.0" # Android 13 baseline - - "Samsung Galaxy S23-13.0" # Popular flagship - - "Google Pixel 6-12.0" # Android 12 support - ios: - devices: - - "iPhone 13-16" # iOS 16 support (min deployment target is 15.6) - - "iPhone 14-16" # iOS 16 current - - "iPhone 12-17" # iOS 17 latest - -# Kotlin Multiplatform QuickStart -kotlin-multiplatform: - android: - devices: - - "Google Pixel 8-14.0" # Latest Android 14 - - "Samsung Galaxy S23-13.0" # Popular flagship - -# React Native QuickStart -react-native: - android: - devices: - - "Samsung Galaxy S22-12.0" # Android 12 - - "Google Pixel 7-13.0" # Android 13 - ios: - devices: - - "iPhone 15-17.0" # Latest iOS 17 - - "iPhone 14-16.0" # iOS 16 support - -# React Native Expo QuickStart -react-native-expo: - android: - devices: - - "Samsung Galaxy S22-12.0" # Android 12 - - "Google Pixel 7-13.0" # Android 13 - ios: - devices: - - "iPhone 15-17.0" # Latest iOS 17 - - "iPhone 14-16.0" # iOS 16 support - -# .NET MAUI QuickStart (multi-platform) -dotnet-maui: - android: - devices: - - "Google Pixel 8-14.0" # Latest Android 14 - - "Samsung Galaxy S23-13.0" # Popular flagship - ios: - devices: - - "iPhone 15-17.0" # Latest iOS 17 - - "iPhone 14-16.0" # iOS 16 support - -# Swift/iOS QuickStart -swift: - devices: - - "iPhone 15 Pro-17" # Latest flagship with iOS 17 - -# Java Spring (web application) -# Tests web app in desktop browsers using Selenium WebDriver -java-spring: - platforms: - - os: "Windows" - osVersion: "11" - browserName: "Chrome" - browserVersion: "latest" # Always test latest Chrome - -# JavaScript Web (React web application) -# Tests web app in multiple desktop browsers -javascript-web: - browsers: - - browser: "Chrome" - browser_version: "120.0" - os: "Windows" - os_version: "11" - - browser: "Firefox" - browser_version: "121.0" - os: "Windows" - os_version: "11" diff --git a/.github/scripts/browserstack-test.py b/.github/scripts/browserstack-test.py deleted file mode 100644 index 24c8265ab..000000000 --- a/.github/scripts/browserstack-test.py +++ /dev/null @@ -1,277 +0,0 @@ -#!/usr/bin/env python3 -""" -BrowserStack cross-browser testing script for Ditto JavaScript Web application. - -This script runs automated tests on multiple browsers using BrowserStack to verify -the basic functionality of the Ditto Tasks web application. -""" - -import time -import sys -import os -import yaml -from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.chrome.options import Options as ChromeOptions -from selenium.webdriver.firefox.options import Options as FirefoxOptions - - -def wait_for_sync_document(driver, doc_id, max_wait=30): - """Wait for a specific document to appear in the task list.""" - print(f"Waiting for document '{doc_id}' to sync...") - print(f"Looking for exact match on document ID/title: {doc_id}") - - start_time = time.time() - - while (time.time() - start_time) < max_wait: - try: - # Based on TaskList.tsx, tasks are rendered as div.group elements containing spans with the title - # Use the most specific selector first - task_elements = driver.find_elements(By.CSS_SELECTOR, "div.group span") - - # Check each element for exact document ID match - for element in task_elements: - try: - element_text = element.text.strip() - # Exact match on the document ID (which is also the title) - if element_text == doc_id: - print(f"āœ“ Found synced document with exact match: {element_text}") - return True - except: - continue - - except Exception as e: - # Only log errors occasionally to reduce noise - pass - - time.sleep(1) # Check every second - - print(f"āŒ Document not found after {max_wait} seconds") - return False - - -def run_test(browser_config): - """Run automated test on specified browser configuration.""" - print( - f"Starting test on {browser_config['browser']} {browser_config['browser_version']} on {browser_config['os']}" - ) - - # Get BrowserStack project and build names from environment - # These are now generated by the get-browserstack-build-info action - project_name = os.environ.get('BROWSERSTACK_PROJECT', 'quickstart - JavaScript Web') - build_name = os.environ.get('BROWSERSTACK_BUILD_NAME', f"Build-{os.environ.get('GITHUB_RUN_NUMBER', '0')}") - - bs_options = { - "browserVersion": browser_config["browser_version"], - "os": browser_config["os"], - "osVersion": browser_config["os_version"], - "sessionName": f"Ditto Tasks Test - {browser_config['browser']} {browser_config['browser_version']}", - "buildName": build_name, - "projectName": project_name, - "local": "true", - "debug": "true", - "video": "true", - "networkLogs": "true", - "consoleLogs": "info", - } - - # Create browser-specific options - if browser_config["browser"].lower() == "chrome": - options = ChromeOptions() - options.set_capability("bstack:options", bs_options) - elif browser_config["browser"].lower() == "firefox": - options = FirefoxOptions() - options.set_capability("bstack:options", bs_options) - else: - # Fallback to Chrome for other browsers - options = ChromeOptions() - options.set_capability("bstack:options", bs_options) - - driver = None - try: - # Initialize WebDriver with modern options - driver = webdriver.Remote( - command_executor=f"https://{os.environ['BROWSERSTACK_USERNAME']}:{os.environ['BROWSERSTACK_ACCESS_KEY']}@hub.browserstack.com/wd/hub", - options=options, - ) - - # Set additional session info - driver.execute_script( - 'browserstack_executor: {"action": "setSessionName", "arguments": {"name": "Ditto Tasks Web Test"}}' - ) - - # Navigate to the application - print("Navigating to application...") - driver.get("http://localhost:3000") - - # Wait for page to load - WebDriverWait(driver, 30).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - - print("Page loaded, waiting for app initialization...") - - # Wait for task input to be enabled - try: - WebDriverWait(driver, 20).until( - lambda d: not d.find_element( - By.CSS_SELECTOR, "input[placeholder*='What needs to be done']" - ).get_attribute("disabled") - ) - print("Task input is enabled") - except: - print("Task input check failed, continuing with basic checks...") - - # Wait a bit for initial sync to complete - print("Waiting for initial sync...") - time.sleep(3) - - # Check for GitHub test document - github_doc_id = os.environ.get("DITTO_CLOUD_TASK_TITLE") - if github_doc_id: - print(f"Checking for GitHub test document: {github_doc_id}") - if wait_for_sync_document(driver, github_doc_id): - print("āœ“ GitHub test document successfully synced from Ditto Cloud") - else: - print("āŒ GitHub test document did not sync within timeout period") - raise Exception("Failed to sync test document from Ditto Cloud") - else: - print("⚠ No GitHub test document ID provided, skipping sync verification") - - # Verify key UI elements are present - print("Verifying UI elements...") - - # Check for task input field - task_input = WebDriverWait(driver, 10).until( - EC.presence_of_element_located( - (By.CSS_SELECTOR, "input[placeholder*='What needs to be done']") - ) - ) - print("āœ“ Task input field found") - - # Check for Add Task button - add_button = driver.find_element( - By.XPATH, "//button[contains(text(), 'Add Task')]" - ) - print("āœ“ Add Task button found") - - # Check for items counter - try: - items_counter = driver.find_element( - By.XPATH, "//*[contains(text(), 'items left') or contains(text(), '0')]" - ) - print(f"āœ“ Items counter found: {items_counter.text}") - except: - print("⚠ Items counter not found or text different than expected") - - # Test basic functionality - add a task - print("Testing task creation...") - - # Create unique task name using GitHub run info and timestamp - github_run_id = os.environ.get("GITHUB_RUN_ID", "local") - github_run_number = os.environ.get("GITHUB_RUN_NUMBER", "0") - timestamp = int(time.time()) - test_task_name = ( - f"BrowserStack Test - Run {github_run_id}#{github_run_number} - {timestamp}" - ) - - task_input.clear() - task_input.send_keys(test_task_name) - add_button.click() - - # Wait a bit for the task to appear - time.sleep(2) - - # Check if task appeared in list - WebDriverWait(driver, 10).until( - EC.presence_of_element_located( - (By.XPATH, f"//*[contains(text(), '{test_task_name}')]") - ) - ) - print(f"āœ“ Task created successfully: {test_task_name}") - - # Take a screenshot for verification - driver.save_screenshot(f"test_screenshot_{browser_config['browser']}.png") - print(f"āœ“ Screenshot saved for {browser_config['browser']}") - - # Report success to BrowserStack - driver.execute_script( - 'browserstack_executor: {"action": "setSessionStatus", "arguments": {"status":"passed", "reason": "All tests passed successfully"}}' - ) - print("āœ“ Reported success status to BrowserStack") - - print(f"āœ… Test completed successfully on {browser_config['browser']}") - return True - - except Exception as e: - print(f"āŒ Test failed on {browser_config['browser']}: {str(e)}") - if driver: - try: - driver.save_screenshot( - f"error_screenshot_{browser_config['browser']}.png" - ) - print(f"Error screenshot saved for {browser_config['browser']}") - - # Report failure to BrowserStack - driver.execute_script( - f'browserstack_executor: {{"action": "setSessionStatus", "arguments": {{"status":"failed", "reason": "Test failed: {str(e)[:100]}"}}}}' - ) - print("āœ“ Reported failure status to BrowserStack") - except: - print("⚠ Failed to save screenshot or report status") - return False - - finally: - if driver: - driver.quit() - - -def main(): - """Main function to run all browser tests.""" - # Load browser configurations from centralized config - config_path = os.path.join( - os.path.dirname(__file__), "..", "browserstack-devices.yml" - ) - with open(config_path, "r") as f: - config = yaml.safe_load(f) - browsers = config["javascript-web"]["browsers"] - - # Run tests on all browsers - results = [] - for browser_config in browsers: - success = run_test(browser_config) - results.append( - { - "browser": f"{browser_config['browser']} {browser_config['browser_version']}", - "os": f"{browser_config['os']} {browser_config['os_version']}", - "success": success, - } - ) - - # Print summary - print("\n=== Test Summary ===") - passed = 0 - total = len(results) - - for result in results: - status = "āœ… PASSED" if result["success"] else "āŒ FAILED" - print(f"{result['browser']} on {result['os']}: {status}") - if result["success"]: - passed += 1 - - print(f"\nOverall: {passed}/{total} tests passed") - - # Exit with appropriate code - if passed == total: - print("šŸŽ‰ All tests passed!") - sys.exit(0) - else: - print("šŸ’„ Some tests failed!") - sys.exit(1) - - -if __name__ == "__main__": - main() - diff --git a/.github/workflows/android-java-ci.yml b/.github/workflows/android-java-ci.yml deleted file mode 100644 index 14f3860ad..000000000 --- a/.github/workflows/android-java-ci.yml +++ /dev/null @@ -1,343 +0,0 @@ -name: Android Java CI - -on: - pull_request: - paths: - - "android-java/**" - - ".github/workflows/android-java-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint (ubuntu-latest) - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - android-java/.gradle - key: gradle-${{ runner.os }}-${{ hashFiles('android-java/gradle/wrapper/gradle-wrapper.properties', 'android-java/**/*.gradle*') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Create test .env file - run: | - echo "DITTO_APP_ID=test" > .env - echo "DITTO_PLAYGROUND_TOKEN=test" >> .env - echo "DITTO_AUTH_URL=test" >> .env - echo "DITTO_WEBSOCKET_URL=test" >> .env - - - name: Run Android linting - working-directory: android-java - run: ./gradlew lint - - build: - name: Build APKs - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 20 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Build APKs - working-directory: android-java - run: ./gradlew assembleDebug assembleDebugAndroidTest - - - name: Upload APK artifacts - uses: actions/upload-artifact@v4 - with: - name: android-apks-${{ github.run_number }} - path: | - android-java/app/build/outputs/apk/debug/app-debug.apk - android-java/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk - retention-days: 1 - - browserstack-android: - name: BrowserStack Device Testing - runs-on: ubuntu-latest - needs: [build] - if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - timeout-minutes: 150 - outputs: - build_id: ${{ steps.test.outputs.build_id }} - - steps: - - uses: actions/checkout@v4 - - - name: Download APK artifacts - uses: actions/download-artifact@v4 - with: - name: android-apks-${{ github.run_number }} - path: android-java/app/build/outputs/apk/ - - - name: Upload APKs to BrowserStack - id: upload - run: | - CREDS="${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" - - # Upload app APK - echo "Uploading app APK to BrowserStack..." - APP_RESPONSE=$(curl -u "$CREDS" \ - -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/app" \ - -F "file=@android-java/app/build/outputs/apk/debug/app-debug.apk" \ - -F "custom_id=ditto-android-java-app") - - APP_URL=$(echo "$APP_RESPONSE" | yq eval -p=json .app_url) - if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then - echo "Error: Failed to upload app APK" - echo "Response: $APP_RESPONSE" - exit 1 - fi - echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT" - echo "App APK uploaded: $APP_URL" - - # Upload test APK - echo "Uploading test APK to BrowserStack..." - TEST_RESPONSE=$(curl -u "$CREDS" \ - -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite" \ - -F "file=@android-java/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk" \ - -F "custom_id=ditto-android-java-test") - - TEST_URL=$(echo "$TEST_RESPONSE" | yq eval -p=json .test_suite_url) - if [ "$TEST_URL" = "null" ] || [ -z "$TEST_URL" ]; then - echo "Error: Failed to upload test APK" - echo "Response: $TEST_RESPONSE" - exit 1 - fi - echo "test_url=$TEST_URL" >> "$GITHUB_OUTPUT" - echo "Test APK uploaded: $TEST_URL" - - - name: Get BrowserStack build info - id: build-info - uses: ./.github/actions/generate-browserstack-names - - - name: Seed and execute tests on BrowserStack - id: test - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_android-java_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_android-java_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_android-java_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.["android-java"].devices' .github/browserstack-devices.yml) - echo "Loaded devices: $DEVICES" - - # Create test execution request - BUILD_RESPONSE=$(curl -s -u '${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}' \ - -X POST 'https://api-cloud.browserstack.com/app-automate/espresso/v2/build' \ - -H 'Content-Type: application/json' \ - -d '{ - "app": "${{ steps.upload.outputs.app_url }}", - "testSuite": "${{ steps.upload.outputs.test_url }}", - "devices": '"$DEVICES"', - "project": "${{ steps.build-info.outputs.project-name }}", - "buildName": "${{ steps.build-info.outputs.build-name }}", - "buildTag": "${{ github.head_ref || github.ref_name }}", - "deviceLogs": true, - "video": true, - "networkLogs": true, - "autoGrantPermissions": true, - "instrumentationLogs": true, - "instrumentationOptions": { - "DITTO_CLOUD_TASK_TITLE": "'"$TASK_TITLE"'" - } - }') - - BUILD_ID=$(echo "$BUILD_RESPONSE" | yq eval .build_id) - - if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then - echo "Error: Failed to create BrowserStack build" - echo "Response: $BUILD_RESPONSE" - exit 1 - fi - - echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT - echo "Build started with ID: $BUILD_ID" - - # Wait for test execution to complete - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=30 # Check every 30 seconds - ELAPSED=0 - - echo "ā³ Waiting for test execution to complete..." - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/$BUILD_ID") - - STATUS=$(echo "$RESPONSE" | yq eval .status) - - if [ "$STATUS" = "null" ] || [ -z "$STATUS" ]; then - echo "āš ļø API error, retrying... (${ELAPSED}s elapsed)" - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - continue - fi - - echo "šŸ“Š Status: $STATUS (${ELAPSED}s elapsed)" - - # Check for completion - if [[ "$STATUS" =~ ^(done|failed|error|passed|completed)$ ]]; then - echo "āœ… Build completed with status: $STATUS" - break - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - # Get final results - FINAL_RESULT=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/$BUILD_ID") - - echo "šŸ“‹ Final results:" - echo "$FINAL_RESULT" | jq . - - # Validate and check results - if echo "$FINAL_RESULT" | jq -e .devices > /dev/null 2>&1; then - BUILD_STATUS=$(echo "$FINAL_RESULT" | yq eval .status) - if [ "$BUILD_STATUS" != "passed" ]; then - echo "āŒ Tests failed with status: $BUILD_STATUS" - - FAILED_DEVICES=$(echo "$FINAL_RESULT" | yq eval '.devices[] | select(.sessions[].status != "passed") | .device') - if [ -n "$FAILED_DEVICES" ]; then - echo "Failed on devices: $FAILED_DEVICES" - fi - exit 1 - else - echo "šŸŽ‰ All tests passed successfully!" - fi - else - echo "āš ļø Could not parse final results" - exit 1 - fi - - - name: Upload test artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: | - android-java/app/build/outputs/apk/ - android-java/app/build/reports/ - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build, browserstack-android] - if: always() - - steps: - - name: Report Results - run: | - echo "## šŸ“± Android Java CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build.result }}" == "success" && \ - "${{ needs.browserstack-android.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| BrowserStack Tests | ${{ needs.browserstack-android.result == 'success' && 'āœ… Passed' || (needs.browserstack-android.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # BrowserStack link - if [[ "${{ needs.browserstack-android.result }}" != "skipped" ]]; then - echo "### BrowserStack Session" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "šŸ¤– [View Test Results](https://app-automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack-android.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Tested Devices:**" >> $GITHUB_STEP_SUMMARY - echo "- Google Pixel 8 (Android 14)" >> $GITHUB_STEP_SUMMARY - echo "- Samsung Galaxy S23 (Android 13)" >> $GITHUB_STEP_SUMMARY - echo "- Google Pixel 6 (Android 12)" >> $GITHUB_STEP_SUMMARY - echo "- OnePlus 9 (Android 11)" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/android-kotlin-ci.yml b/.github/workflows/android-kotlin-ci.yml deleted file mode 100644 index 6cba4a704..000000000 --- a/.github/workflows/android-kotlin-ci.yml +++ /dev/null @@ -1,361 +0,0 @@ -name: Android Kotlin CI - -on: - pull_request: - paths: - - "android-kotlin/**" - - ".github/workflows/android-kotlin-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint (ubuntu-latest) - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - android-kotlin/QuickStartTasks/.gradle - key: gradle-${{ runner.os }}-${{ hashFiles('android-kotlin/QuickStartTasks/gradle/wrapper/gradle-wrapper.properties', 'android-kotlin/QuickStartTasks/**/*.gradle*', 'android-kotlin/QuickStartTasks/gradle/libs.versions.toml') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Create test .env file - run: | - echo "DITTO_APP_ID=test" > .env - echo "DITTO_PLAYGROUND_TOKEN=test" >> .env - echo "DITTO_AUTH_URL=test" >> .env - echo "DITTO_WEBSOCKET_URL=test" >> .env - - - name: Run Android linting - working-directory: android-kotlin/QuickStartTasks - run: ./gradlew lint - - build: - name: Build and Test - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 30 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Build APKs - working-directory: android-kotlin/QuickStartTasks - env: - DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} - DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} - DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} - DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} - run: ./gradlew assembleDebug assembleDebugAndroidTest - - - name: Run unit tests - working-directory: android-kotlin/QuickStartTasks - env: - DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} - DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} - DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} - DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} - run: ./gradlew test - - - name: Upload APK artifacts - uses: actions/upload-artifact@v4 - with: - name: android-apks-${{ github.run_number }} - path: | - android-kotlin/QuickStartTasks/app/build/outputs/apk/debug/app-debug.apk - android-kotlin/QuickStartTasks/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk - retention-days: 1 - - - name: Upload test reports - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-reports-${{ github.run_number }} - path: android-kotlin/QuickStartTasks/app/build/reports/ - retention-days: 1 - - browserstack-android: - name: BrowserStack Device Testing - runs-on: ubuntu-latest - needs: [build] - if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - timeout-minutes: 150 - outputs: - build_id: ${{ steps.test.outputs.build_id }} - - steps: - - uses: actions/checkout@v4 - - - name: Download APK artifacts - uses: actions/download-artifact@v4 - with: - name: android-apks-${{ github.run_number }} - path: android-kotlin/QuickStartTasks/app/build/outputs/apk/ - - - name: Upload APKs to BrowserStack - id: upload - run: | - CREDS="${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" - - # Upload app APK - echo "Uploading app APK to BrowserStack..." - APP_RESPONSE=$(curl -u "$CREDS" \ - -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/app" \ - -F "file=@android-kotlin/QuickStartTasks/app/build/outputs/apk/debug/app-debug.apk" \ - -F "custom_id=ditto-android-kotlin-app") - - APP_URL=$(echo "$APP_RESPONSE" | yq eval -p=json .app_url) - if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then - echo "Error: Failed to upload app APK" - echo "Response: $APP_RESPONSE" - exit 1 - fi - echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT" - echo "App APK uploaded: $APP_URL" - - # Upload test APK - echo "Uploading test APK to BrowserStack..." - TEST_RESPONSE=$(curl -u "$CREDS" \ - -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite" \ - -F "file=@android-kotlin/QuickStartTasks/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk" \ - -F "custom_id=ditto-android-kotlin-test") - - TEST_URL=$(echo "$TEST_RESPONSE" | yq eval -p=json .test_suite_url) - if [ "$TEST_URL" = "null" ] || [ -z "$TEST_URL" ]; then - echo "Error: Failed to upload test APK" - echo "Response: $TEST_RESPONSE" - exit 1 - fi - echo "test_url=$TEST_URL" >> "$GITHUB_OUTPUT" - echo "Test APK uploaded: $TEST_URL" - - - name: Get BrowserStack build info - id: build-info - uses: ./.github/actions/generate-browserstack-names - - - name: Seed and execute tests on BrowserStack - id: test - uses: nick-fields/retry@v3 - env: - BUILD_NAME: ${{ steps.build-info.outputs.build-name }} - PROJECT_NAME: ${{ steps.build-info.outputs.project-name }} - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_android-kotlin_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_android-kotlin_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_android-kotlin_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.["android-kotlin"].devices' .github/browserstack-devices.yml) - - # Use jq to properly escape the build name for JSON - BUILD_NAME_JSON=$(echo "$BUILD_NAME" | jq -Rs .) - PROJECT_NAME_JSON=$(echo "$PROJECT_NAME" | jq -Rs .) - - BUILD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/build" \ - -H "Content-Type: application/json" \ - -d "{ - \"app\": \"${{ steps.upload.outputs.app_url }}\", - \"testSuite\": \"${{ steps.upload.outputs.test_url }}\", - \"devices\": $DEVICES, - \"project\": $PROJECT_NAME_JSON, - \"buildName\": $BUILD_NAME_JSON, - \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", - \"deviceLogs\": true, - \"video\": true, - \"networkLogs\": true, - \"autoGrantPermissions\": true, - \"instrumentationOptions\": { - \"DITTO_CLOUD_TASK_TITLE\": \"$TASK_TITLE\" - } - }") - - echo "BrowserStack API Response:" - echo "$BUILD_RESPONSE" - - BUILD_ID=$(echo "$BUILD_RESPONSE" | yq eval .build_id) - - # Check if BUILD_ID is null or empty - if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then - echo "Error: Failed to create BrowserStack build" - echo "Response: $BUILD_RESPONSE" - exit 1 - fi - - echo "Build started with ID: $BUILD_ID" - echo "build_id=$BUILD_ID" >> "$GITHUB_OUTPUT" - - # Wait for BrowserStack tests to complete - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=30 # Check every 30 seconds - ELAPSED=0 - - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - BUILD_STATUS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/$BUILD_ID") - - BUILD_STATUS=$(echo "$BUILD_STATUS_RESPONSE" | yq eval .status) - - # Check for API errors - if [ "$BUILD_STATUS" = "null" ] || [ -z "$BUILD_STATUS" ]; then - echo "Error getting build status. Response: $BUILD_STATUS_RESPONSE" - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - continue - fi - - echo "Build status: $BUILD_STATUS (elapsed: ${ELAPSED}s)" - - # Check for completion states - BrowserStack uses different status values - if [ "$BUILD_STATUS" = "done" ] || [ "$BUILD_STATUS" = "failed" ] || [ "$BUILD_STATUS" = "error" ] || [ "$BUILD_STATUS" = "passed" ] || [ "$BUILD_STATUS" = "completed" ]; then - echo "Build completed with status: $BUILD_STATUS" - break - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - # Get final results - FINAL_RESULT=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/$BUILD_ID") - - echo "Final build result:" - echo "$FINAL_RESULT" | jq . - - # Check if we got valid results - if echo "$FINAL_RESULT" | jq -e .devices > /dev/null 2>&1; then - # Check if the overall build passed - BUILD_STATUS=$(echo "$FINAL_RESULT" | yq eval .status) - if [ "$BUILD_STATUS" != "passed" ]; then - echo "Build failed with status: $BUILD_STATUS" - - # Check each device for failures - FAILED_TESTS=$(echo "$FINAL_RESULT" | yq eval '.devices[] | select(.sessions[].status != "passed") | .device') - - if [ -n "$FAILED_TESTS" ]; then - echo "Tests failed on devices: $FAILED_TESTS" - fi - - exit 1 - else - echo "All tests passed successfully!" - fi - else - echo "Warning: Could not parse final results" - echo "Raw response: $FINAL_RESULT" - fi - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build, browserstack-android] - if: always() - - steps: - - name: Report Results - run: | - echo "## šŸ“± Android Kotlin CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build.result }}" == "success" && \ - "${{ needs.browserstack-android.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build and Test | ${{ needs.build.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| BrowserStack Tests | ${{ needs.browserstack-android.result == 'success' && 'āœ… Passed' || (needs.browserstack-android.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # BrowserStack link - if [[ "${{ needs.browserstack-android.result }}" != "skipped" ]]; then - echo "### BrowserStack Session" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "šŸ¤– [View Test Results](https://app-automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack-android.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Tested Devices:**" >> $GITHUB_STEP_SUMMARY - echo "- Google Pixel 8 (Android 14)" >> $GITHUB_STEP_SUMMARY - echo "- Samsung Galaxy S23 (Android 13)" >> $GITHUB_STEP_SUMMARY - echo "- Google Pixel 6 (Android 12)" >> $GITHUB_STEP_SUMMARY - echo "- OnePlus 9 (Android 11)" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/cpp-tui-ci.yml b/.github/workflows/cpp-tui-ci.yml deleted file mode 100644 index 89c5f8912..000000000 --- a/.github/workflows/cpp-tui-ci.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: C++ TUI CI -on: - pull_request: - paths: - - 'cpp-tui/**' - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y clang-format - - - name: Run clang-format check - working-directory: cpp-tui/taskscpp - run: | - make format - if ! git diff --exit-code; then - echo "āŒ Code style issues found. Run 'make format' to fix." - exit 1 - fi - - build: - name: Build - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y cmake clang build-essential - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Download and setup Ditto C++ SDK - working-directory: cpp-tui/taskscpp - run: make download-sdk - - - name: Build application - working-directory: cpp-tui/taskscpp - run: | - awk -f scripts/generate_env.awk ../../.env > src/env.h - make build - - integration-test: - name: Integration Test - runs-on: ubuntu-latest - needs: [lint, build] - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y cmake clang build-essential - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Seed test document to Ditto Cloud - id: seed - uses: ./.github/actions/seed-ditto-document - with: - ditto-api-key: ${{ secrets.DITTO_API_KEY }} - ditto-api-url: ${{ secrets.DITTO_API_URL }} - app-name: 'cpp-tui' - - - name: Download and setup Ditto C++ SDK - working-directory: cpp-tui/taskscpp - run: make download-sdk - - - name: Build and run integration test - working-directory: cpp-tui/taskscpp - env: - DITTO_CLOUD_TASK_TITLE: ${{ steps.seed.outputs.document-title }} - GITHUB_RUN_ID: ${{ github.run_id }} - GITHUB_RUN_NUMBER: ${{ github.run_number }} - run: | - awk -f scripts/generate_env.awk ../../.env > src/env.h - mkdir -p build - g++ -std=c++17 -I./src -I./sdk -I./third_party/cxxopts/include \ - tests/integration_test.cpp \ - src/task.cpp src/tasks_peer.cpp src/tasks_log.cpp \ - -L./sdk -lditto -ldl -lrt -pthread \ - -o build/integration_test - ./build/integration_test - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build, integration-test] - if: always() - - steps: - - name: Report Results - run: | - echo "## šŸ”§ C++ TUI CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build.result }}" == "success" && \ - "${{ needs.integration-test.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Integration Test | ${{ needs.integration-test.result == 'success' && 'āœ… Passed' || (needs.integration-test.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/dotnet-maui-ci.yml b/.github/workflows/dotnet-maui-ci.yml deleted file mode 100644 index 7fef8422d..000000000 --- a/.github/workflows/dotnet-maui-ci.yml +++ /dev/null @@ -1,450 +0,0 @@ -name: .NET MAUI CI - -on: - pull_request: - paths: - - "dotnet-maui/**" - - ".github/workflows/dotnet-maui-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - global-json-file: dotnet-maui/global.json - - - name: Create test .env file - run: | - echo "DITTO_APP_ID=test" > .env - echo "DITTO_PLAYGROUND_TOKEN=test" >> .env - echo "DITTO_AUTH_URL=test" >> .env - echo "DITTO_WEBSOCKET_URL=test" >> .env - - - name: Restore workloads and dependencies - working-directory: dotnet-maui/DittoMauiTasksApp - run: | - dotnet workload restore - dotnet restore - - - name: Run .NET format check - working-directory: dotnet-maui/DittoMauiTasksApp - run: | - dotnet format DittoMauiTasksApp.sln --verify-no-changes --verbosity diagnostic - if [ $? -ne 0 ]; then - echo "āŒ Code formatting issues found. Run 'dotnet format' to fix." - exit 1 - fi - - build-android: - name: Build Android - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 20 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - global-json-file: dotnet-maui/global.json - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Restore workloads and dependencies - working-directory: dotnet-maui/DittoMauiTasksApp - run: | - dotnet workload restore - dotnet restore - - - name: Build Android APK (unsigned Release) - working-directory: dotnet-maui/DittoMauiTasksApp - run: | - dotnet build DittoMauiTasksApp.csproj --configuration Release --framework net10.0-android --no-restore - - - name: Upload Android APK artifact - uses: actions/upload-artifact@v4 - with: - name: android-apk - path: dotnet-maui/DittoMauiTasksApp/bin/Release/net10.0-android/live.ditto.quickstart.mauitasksapp-Signed.apk - retention-days: 1 - - build-ios: - name: Build iOS - runs-on: macos-26 - needs: lint - timeout-minutes: 60 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "26.3" - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - global-json-file: dotnet-maui/global.json - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Restore workloads and dependencies - working-directory: dotnet-maui/DittoMauiTasksApp - run: | - dotnet workload restore - dotnet restore --runtime ios-arm64 - - - name: Build iOS Device (Release for BrowserStack) - working-directory: dotnet-maui/DittoMauiTasksApp - run: | - dotnet build DittoMauiTasksApp.csproj --configuration Release --framework net10.0-ios --no-restore \ - /p:RuntimeIdentifier=ios-arm64 \ - /p:EnableCodeSigning=false \ - /p:ArchiveOnBuild=false - - - name: Create iOS IPA for BrowserStack - working-directory: dotnet-maui/DittoMauiTasksApp/bin/Release/net10.0-ios/ios-arm64 - run: | - mkdir -p Payload - cp -r DittoMauiTasksApp.app Payload/ - zip -r DittoMauiTasksApp-device.ipa Payload - echo "Created IPA: $(pwd)/DittoMauiTasksApp-device.ipa" - ls -la DittoMauiTasksApp-device.ipa - - - name: Upload iOS IPA artifact - uses: actions/upload-artifact@v4 - with: - name: ios-ipa - path: dotnet-maui/DittoMauiTasksApp/bin/Release/net10.0-ios/ios-arm64/DittoMauiTasksApp-device.ipa - retention-days: 1 - - browserstack-android: - name: BrowserStack Android Tests - runs-on: ubuntu-latest - needs: [build-android] - timeout-minutes: 150 - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') - outputs: - build_id: ${{ steps.test.outputs.build_id }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - global-json-file: dotnet-maui/global.json - - - name: Download Android APK artifact - uses: actions/download-artifact@v4 - with: - name: android-apk - path: ./android-artifact - - - name: Upload APK to BrowserStack - id: upload - run: | - echo "Uploading Android APK to BrowserStack..." - APK_PATH="./android-artifact/live.ditto.quickstart.mauitasksapp-Signed.apk" - if [ ! -f "$APK_PATH" ]; then - echo "Error: APK not found at $APK_PATH" - find ./android-artifact -name "*.apk" -type f - exit 1 - fi - - UPLOAD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ - -F "file=@$APK_PATH" \ - -F "custom_id=ditto-dotnet-maui-android") - - APP_URL=$(echo "$UPLOAD_RESPONSE" | yq eval -p=json .app_url) - if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then - echo "Error: Failed to upload APK" - exit 1 - fi - echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT" - echo "Android APK uploaded: $APP_URL" - - - name: Get BrowserStack build info (Android) - id: build-info-android - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (Android)' - title-max-length: '90' - commit-max-length: '130' - - - name: Seed and execute Android tests on BrowserStack - id: test - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_dotnet-maui_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_dotnet-maui_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_dotnet-maui_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Load device from centralized config - DEVICE=$(yq eval '.["dotnet-maui"].android.devices[0]' .github/browserstack-devices.yml) - echo "Using Android device: $DEVICE" - - # Execute tests - cd dotnet-maui/UITests.Android - dotnet restore - BROWSERSTACK_USERNAME="${{ secrets.BROWSERSTACK_USERNAME }}" \ - BROWSERSTACK_ACCESS_KEY="${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - BROWSERSTACK_APP_ID="${{ steps.upload.outputs.app_url }}" \ - BROWSERSTACK_DEVICE="$DEVICE" \ - DITTO_CLOUD_TASK_TITLE="$TASK_TITLE" \ - BROWSERSTACK_PROJECT="${{ steps.build-info-android.outputs.project-name }}" \ - BUILD_NAME="${{ steps.build-info-android.outputs.build-name }}" \ - dotnet run - - # Query BrowserStack API to get the build ID - echo "Fetching build ID from BrowserStack..." - BUILDS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/builds.json?limit=1") - - BUILD_ID=$(echo "$BUILDS_RESPONSE" | yq eval '.[0].hashed_id' -) - - if [ "$BUILD_ID" != "null" ] && [ -n "$BUILD_ID" ]; then - echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT - echo "āœ… Build ID: $BUILD_ID" - else - echo "āš ļø Could not retrieve build ID from BrowserStack API" - fi - - browserstack-ios: - name: BrowserStack iOS Tests - runs-on: macos-latest - needs: [build-ios] - timeout-minutes: 150 - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') - outputs: - build_id: ${{ steps.test.outputs.build_id }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - global-json-file: dotnet-maui/global.json - - - name: Download iOS IPA artifact - uses: actions/download-artifact@v4 - with: - name: ios-ipa - path: ./ios-artifact - - - name: Upload IPA to BrowserStack - id: upload - run: | - echo "Uploading iOS IPA to BrowserStack..." - IPA_PATH="./ios-artifact/DittoMauiTasksApp-device.ipa" - if [ ! -f "$IPA_PATH" ]; then - echo "Error: IPA not found at $IPA_PATH" - find ./ios-artifact -name "*.ipa" -type f - exit 1 - fi - - UPLOAD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ - -F "file=@$IPA_PATH" \ - -F "custom_id=ditto-dotnet-maui-ios") - - APP_URL=$(echo "$UPLOAD_RESPONSE" | yq eval -p=json .app_url) - if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then - echo "Error: Failed to upload IPA" - exit 1 - fi - echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT" - echo "iOS IPA uploaded: $APP_URL" - - - name: Get BrowserStack build info (iOS) - id: build-info-ios - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (iOS)' - title-max-length: '90' - commit-max-length: '130' - - - name: Seed and execute iOS tests on BrowserStack - id: test - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_dotnet-maui_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_dotnet-maui_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_dotnet-maui_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Load device from centralized config - DEVICE=$(yq eval '.["dotnet-maui"].ios.devices[0]' .github/browserstack-devices.yml) - echo "Using iOS device: $DEVICE" - - # Execute tests - cd dotnet-maui/UITests.iOS - dotnet restore - BROWSERSTACK_USERNAME="${{ secrets.BROWSERSTACK_USERNAME }}" \ - BROWSERSTACK_ACCESS_KEY="${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - BROWSERSTACK_APP_ID="${{ steps.upload.outputs.app_url }}" \ - BROWSERSTACK_DEVICE="$DEVICE" \ - DITTO_CLOUD_TASK_TITLE="$TASK_TITLE" \ - BROWSERSTACK_PROJECT="${{ steps.build-info-ios.outputs.project-name }}" \ - BUILD_NAME="${{ steps.build-info-ios.outputs.build-name }}" \ - dotnet run - - # Query BrowserStack API to get the build ID - echo "Fetching build ID from BrowserStack..." - BUILDS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/builds.json?limit=1") - - BUILD_ID=$(echo "$BUILDS_RESPONSE" | yq eval '.[0].hashed_id' -) - - if [ "$BUILD_ID" != "null" ] && [ -n "$BUILD_ID" ]; then - echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT - echo "āœ… Build ID: $BUILD_ID" - else - echo "āš ļø Could not retrieve build ID from BrowserStack API" - fi - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build-android, build-ios, browserstack-android, browserstack-ios] - if: always() - - steps: - - name: Report Results - run: | - echo "## šŸ“± .NET MAUI CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build-android.result }}" == "success" && \ - "${{ needs.build-ios.result }}" == "success" && \ - "${{ needs.browserstack-android.result }}" == "success" && \ - "${{ needs.browserstack-ios.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build Android | ${{ needs.build-android.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build iOS | ${{ needs.build-ios.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| BrowserStack Android | ${{ needs.browserstack-android.result == 'success' && 'āœ… Passed' || (needs.browserstack-android.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| BrowserStack iOS | ${{ needs.browserstack-ios.result == 'success' && 'āœ… Passed' || (needs.browserstack-ios.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # BrowserStack links - echo "### BrowserStack Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ needs.browserstack-android.result }}" != "skipped" && -n "${{ needs.browserstack-android.outputs.build_id }}" ]]; then - echo "**šŸ¤– Android:**" >> $GITHUB_STEP_SUMMARY - echo "šŸ”— [View Android Test Results](https://app-automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack-android.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - if [[ "${{ needs.browserstack-ios.result }}" != "skipped" && -n "${{ needs.browserstack-ios.outputs.build_id }}" ]]; then - echo "**šŸŽ iOS:**" >> $GITHUB_STEP_SUMMARY - echo "šŸ”— [View iOS Test Results](https://app-automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack-ios.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/dotnet-tui-ci.yml b/.github/workflows/dotnet-tui-ci.yml deleted file mode 100644 index 46d835b9b..000000000 --- a/.github/workflows/dotnet-tui-ci.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: .NET TUI CI -on: - pull_request: - paths: - - 'dotnet-tui/**' - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Create .env file - run: | - echo "DITTO_APP_ID=dummy_id" > .env - echo "DITTO_PLAYGROUND_TOKEN=dummy_token" >> .env - echo "DITTO_AUTH_URL=https://auth.cloud.ditto.live" >> .env - echo "DITTO_WEBSOCKET_URL=wss://cloud.ditto.live" >> .env - - - name: Run dotnet format check - working-directory: dotnet-tui - run: | - dotnet format DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj --verify-no-changes --verbosity diagnostic - if [ $? -ne 0 ]; then - echo "āŒ Code formatting issues found. Run 'dotnet format' to fix." - exit 1 - fi - - build: - name: Build - strategy: - matrix: - os: ["macos-latest", "ubuntu-latest", "windows-latest"] - runs-on: ${{ matrix.os }} - needs: lint - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Create .env file - run: | - echo "DITTO_APP_ID=dummy_id" > .env - echo "DITTO_PLAYGROUND_TOKEN=dummy_token" >> .env - echo "DITTO_AUTH_URL=https://auth.cloud.ditto.live" >> .env - echo "DITTO_WEBSOCKET_URL=wss://cloud.ditto.live" >> .env - shell: bash - - - name: Build project - working-directory: dotnet-tui - run: dotnet build DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj --configuration Release - - integration-test: - name: Integration Test - runs-on: ubuntu-latest - needs: [lint, build] - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Seed test document to Ditto Cloud - id: seed - uses: ./.github/actions/seed-ditto-document - with: - ditto-api-key: ${{ secrets.DITTO_API_KEY }} - ditto-api-url: ${{ secrets.DITTO_API_URL }} - app-name: 'dotnet-tui' - - - name: Build .NET application - working-directory: dotnet-tui - run: dotnet build DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj --configuration Release - - - name: Run .NET integration tests - working-directory: dotnet-tui - env: - DITTO_CLOUD_TASK_TITLE: ${{ steps.seed.outputs.document-title }} - GITHUB_RUN_ID: ${{ github.run_id }} - GITHUB_RUN_NUMBER: ${{ github.run_number }} - run: dotnet test DittoDotNetTasksConsole.Tests --configuration Release --logger "console;verbosity=detailed" - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build, integration-test] - if: always() - - steps: - - name: Report Results - run: | - echo "## šŸ”· .NET TUI CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build.result }}" == "success" && \ - "${{ needs.integration-test.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Integration Test | ${{ needs.integration-test.result == 'success' && 'āœ… Passed' || (needs.integration-test.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/dotnet-winforms-ci.yml b/.github/workflows/dotnet-winforms-ci.yml deleted file mode 100644 index 3a43243ed..000000000 --- a/.github/workflows/dotnet-winforms-ci.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: .NET WinForms CI -on: - pull_request: - paths: - - 'dotnet-winforms/**' - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint - runs-on: windows-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: Install dotnet-format - run: dotnet tool install -g dotnet-format - - - name: Run dotnet-format check - working-directory: dotnet-winforms/TasksApp - run: | - dotnet format DittoTasksApp.sln --verify-no-changes --verbosity diagnostic - if ($LASTEXITCODE -ne 0) { - Write-Host "āŒ Code formatting issues found. Run 'dotnet format' to fix." - exit 1 - } - - build: - name: Build - runs-on: windows-latest - needs: lint - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Restore dependencies - working-directory: dotnet-winforms/TasksApp - run: dotnet restore - - - name: Build application - working-directory: dotnet-winforms/TasksApp - run: dotnet build --configuration Release --no-restore - - integration-test: - name: Integration Test - runs-on: windows-latest - needs: [lint, build] - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Seed test document to Ditto Cloud - id: seed - uses: ./.github/actions/seed-ditto-document - with: - ditto-api-key: ${{ secrets.DITTO_API_KEY }} - ditto-api-url: ${{ secrets.DITTO_API_URL }} - app-name: 'dotnet-winforms' - - - name: Build and run integration test - working-directory: dotnet-winforms/IntegrationTest - env: - DITTO_CLOUD_TASK_TITLE: ${{ steps.seed.outputs.document-title }} - run: | - dotnet restore - dotnet build --configuration Release --no-restore - dotnet run --configuration Release --no-build - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build, integration-test] - if: always() - - steps: - - name: Report Results - run: | - echo "## 🪟 .NET WinForms CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build.result }}" == "success" && \ - "${{ needs.integration-test.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Integration Test | ${{ needs.integration-test.result == 'success' && 'āœ… Passed' || (needs.integration-test.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml deleted file mode 100644 index 35823b11d..000000000 --- a/.github/workflows/flutter-ci.yml +++ /dev/null @@ -1,948 +0,0 @@ -name: Flutter CI - -on: - pull_request: - paths: - - "flutter_app/**" - - ".github/workflows/flutter-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Create .env file for linting - run: | - echo "DITTO_APP_ID=test" > flutter_app/.env - echo "DITTO_PLAYGROUND_TOKEN=test" >> flutter_app/.env - echo "DITTO_AUTH_URL=test" >> flutter_app/.env - echo "DITTO_WEBSOCKET_URL=test" >> flutter_app/.env - - - name: Get dependencies - working-directory: flutter_app - run: flutter pub get - - - name: Check formatting - working-directory: flutter_app - run: | - dart format --set-exit-if-changed . - if [ $? -ne 0 ]; then - echo "āŒ Code formatting issues found. Run 'dart format .' to fix." - exit 1 - fi - - - name: Analyze code - working-directory: flutter_app - run: flutter analyze --no-fatal-infos - - build-android: - name: Build Android - runs-on: ubuntu-latest - needs: [lint, unit-tests] - timeout-minutes: 20 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env - - - name: Cache Flutter dependencies - uses: actions/cache@v4 - with: - path: | - ~/.pub-cache - flutter_app/.dart_tool - flutter_app/build - key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }} - restore-keys: | - flutter-${{ runner.os }}- - - - name: Get dependencies - working-directory: flutter_app - run: flutter pub get - - - name: Build Android APK (sanity check) - working-directory: flutter_app - run: flutter build apk --release - - - name: Upload APK artifact - uses: actions/upload-artifact@v4 - with: - name: flutter-android-apk-${{ github.run_number }} - path: flutter_app/build/app/outputs/flutter-apk/app-release.apk - retention-days: 1 - - build-ios: - name: Build iOS - runs-on: macos-latest - needs: [lint, unit-tests] - timeout-minutes: 30 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env - - - name: Cache Flutter dependencies - uses: actions/cache@v4 - with: - path: | - ~/.pub-cache - flutter_app/.dart_tool - flutter_app/build - key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }} - restore-keys: | - flutter-${{ runner.os }}- - - - name: Cache CocoaPods dependencies - uses: actions/cache@v4 - with: - path: flutter_app/ios/Pods - key: pods-${{ runner.os }}-${{ hashFiles('flutter_app/ios/Podfile.lock') }} - restore-keys: | - pods-${{ runner.os }}- - - - name: Get dependencies - working-directory: flutter_app - run: flutter pub get - - - name: Install CocoaPods - working-directory: flutter_app/ios - run: pod install || pod install --repo-update - - - name: Build iOS (no signing) - working-directory: flutter_app - run: flutter build ios --release --no-codesign - - - name: Create unsigned IPA - working-directory: flutter_app - run: | - mkdir -p build/ios/iphoneos/Payload - cp -r build/ios/iphoneos/Runner.app build/ios/iphoneos/Payload/ - cd build/ios/iphoneos - zip -r Runner-unsigned.ipa Payload/ - echo "āœ… Created unsigned IPA" - - - name: Upload iOS build artifact - uses: actions/upload-artifact@v4 - with: - name: flutter-ios-ipa-${{ github.run_number }} - path: flutter_app/build/ios/iphoneos/Runner-unsigned.ipa - retention-days: 1 - - build-web: - name: Build Web - runs-on: ubuntu-latest - needs: [lint, unit-tests] - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env - - - name: Cache Flutter dependencies - uses: actions/cache@v4 - with: - path: | - ~/.pub-cache - flutter_app/.dart_tool - flutter_app/build - key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }} - restore-keys: | - flutter-${{ runner.os }}- - - - name: Get dependencies - working-directory: flutter_app - run: flutter pub get - - - name: Build Web - working-directory: flutter_app - run: flutter build web --release - - - name: Upload Web build artifact - uses: actions/upload-artifact@v4 - with: - name: flutter-web-${{ github.run_number }} - path: flutter_app/build/web/ - retention-days: 1 - - build-macos: - name: Build macOS Desktop - runs-on: macos-latest - needs: [lint, unit-tests] - timeout-minutes: 20 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env - - - name: Cache Flutter dependencies - uses: actions/cache@v4 - with: - path: | - ~/.pub-cache - flutter_app/.dart_tool - flutter_app/build - key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }} - restore-keys: | - flutter-${{ runner.os }}- - - - name: Get dependencies - working-directory: flutter_app - run: flutter pub get - - - name: Build macOS Desktop - working-directory: flutter_app - run: flutter build macos --release - - - name: Upload macOS build artifact - uses: actions/upload-artifact@v4 - with: - name: flutter-macos-${{ github.run_number }} - path: flutter_app/build/macos/Build/Products/Release/ - retention-days: 1 - - build-windows: - name: Build Windows Desktop - runs-on: windows-latest - needs: [lint, unit-tests] - timeout-minutes: 20 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env - - - name: Cache Flutter dependencies - uses: actions/cache@v4 - with: - path: | - ~\AppData\Local\Pub\Cache - flutter_app\.dart_tool - flutter_app\build - key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }} - restore-keys: | - flutter-${{ runner.os }}- - - - name: Get dependencies - working-directory: flutter_app - run: flutter pub get - - - name: Build Windows Desktop - working-directory: flutter_app - run: flutter build windows --release - - - name: Upload Windows build artifact - uses: actions/upload-artifact@v4 - with: - name: flutter-windows-${{ github.run_number }} - path: flutter_app/build/windows/x64/runner/Release/ - retention-days: 1 - - unit-tests: - name: Run Unit Tests - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Create .env file - run: | - echo "DITTO_APP_ID=test" > flutter_app/.env - echo "DITTO_PLAYGROUND_TOKEN=test" >> flutter_app/.env - echo "DITTO_AUTH_URL=test" >> flutter_app/.env - echo "DITTO_WEBSOCKET_URL=test" >> flutter_app/.env - - - name: Get dependencies - working-directory: flutter_app - run: flutter pub get - - - name: Run tests - working-directory: flutter_app - run: | - if [ -d "test" ]; then - flutter test --coverage - else - echo "āš ļø No tests found. Skipping test execution." - fi - - - name: Upload coverage reports - if: always() - uses: actions/upload-artifact@v4 - with: - name: flutter-coverage-${{ github.run_number }} - path: flutter_app/coverage/ - retention-days: 1 - - browserstack-android: - name: BrowserStack Android Testing - runs-on: ubuntu-latest - needs: [build-android] - if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - timeout-minutes: 150 - outputs: - build_id: ${{ steps.test.outputs.build_id }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - flutter_app/android/.gradle - key: gradle-${{ runner.os }}-${{ hashFiles('flutter_app/android/gradle/wrapper/gradle-wrapper.properties', 'flutter_app/android/build.gradle.kts', 'flutter_app/android/app/build.gradle.kts') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Get dependencies - working-directory: flutter_app - run: flutter pub get - - - name: Get BrowserStack build info (Android) - id: build-info-android - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (Android)' - title-max-length: '90' - commit-max-length: '130' - - - name: Seed, build, upload, and execute BrowserStack Android tests - id: test - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "šŸ“ Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "āœ… Document inserted successfully with title: $TASK_TITLE" - else - echo "āŒ Failed to insert document. HTTP status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Build Android debug APKs with seeded task title - cd flutter_app/android - ENCODED_TASK=$(echo -n "TASK_TO_FIND=$TASK_TITLE" | base64) - TARGET_PATH="$(pwd)/../integration_test/app_test.dart" - - echo "šŸ“¦ Building Android debug APKs with task title: $TASK_TITLE" - echo "šŸ“¦ Encoded: $ENCODED_TASK" - echo "šŸ“¦ Target: $TARGET_PATH" - - chmod +x gradlew - ./gradlew assembleDebug assembleDebugAndroidTest \ - -Ptarget="$TARGET_PATH" \ - -Pdart-defines="SU5URUdSQVRJT05fVEVTVF9NT0RFPXRydWU=,$ENCODED_TASK" - - cd ../.. - - # Upload app APK to BrowserStack - echo "šŸ“¤ Uploading app APK to BrowserStack..." - APP_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/android/app" \ - -F "file=@flutter_app/build/app/outputs/apk/debug/app-debug.apk") - - echo "App upload response: $APP_RESPONSE" - APP_URL=$(echo "$APP_RESPONSE" | yq eval -p=json .app_url) - - if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then - echo "āŒ Failed to upload app APK" - echo "Response: $APP_RESPONSE" - exit 1 - fi - echo "āœ… App APK uploaded: $APP_URL" - - # Upload test suite APK to BrowserStack - echo "šŸ“¤ Uploading test suite APK to BrowserStack..." - TEST_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/android/test-suite" \ - -F "file=@flutter_app/build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk") - - echo "Test suite upload response: $TEST_RESPONSE" - TEST_URL=$(echo "$TEST_RESPONSE" | yq eval -p=json .test_suite_url) - - if [ "$TEST_URL" = "null" ] || [ -z "$TEST_URL" ]; then - echo "āŒ Failed to upload test suite APK" - echo "Response: $TEST_RESPONSE" - exit 1 - fi - echo "āœ… Test suite APK uploaded: $TEST_URL" - - # Execute tests on BrowserStack - echo "šŸš€ Executing tests on BrowserStack..." - - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.flutter.android.devices' .github/browserstack-devices.yml) - - # Create test execution request - BUILD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/android/build" \ - -H "Content-Type: application/json" \ - -d "{ - \"app\": \"$APP_URL\", - \"testSuite\": \"$TEST_URL\", - \"devices\": $DEVICES, - \"project\": \"${{ steps.build-info-android.outputs.project-name }}\", - \"buildName\": \"${{ steps.build-info-android.outputs.build-name }}\", - \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", - \"deviceLogs\": true, - \"video\": true, - \"networkLogs\": true, - \"autoGrantPermissions\": true - }") - - echo "BrowserStack API Response:" - echo "$BUILD_RESPONSE" - - BUILD_ID=$(echo "$BUILD_RESPONSE" | yq eval .build_id) - - # Check if BUILD_ID is null or empty - if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then - echo "Error: Failed to create BrowserStack build" - echo "Response: $BUILD_RESPONSE" - exit 1 - fi - - echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT - echo "Build started with ID: $BUILD_ID" - - # Wait for tests to complete - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=30 # Check every 30 seconds - ELAPSED=0 - - echo "ā³ Waiting for test execution to complete..." - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/android/builds/$BUILD_ID") - - STATUS=$(echo "$RESPONSE" | yq eval .status) - - if [ "$STATUS" = "null" ] || [ -z "$STATUS" ]; then - echo "āš ļø API error, retrying... (${ELAPSED}s elapsed)" - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - continue - fi - - # Check device statuses - if all devices are done, we're done - DEVICE_STATUSES=$(echo "$RESPONSE" | yq eval '.devices[]?.sessions[]?.status' 2>/dev/null) - if [ -n "$DEVICE_STATUSES" ]; then - ALL_DONE=true - while IFS= read -r dev_status; do - if [[ ! "$dev_status" =~ ^(passed|failed|error|done)$ ]]; then - ALL_DONE=false - break - fi - done <<< "$DEVICE_STATUSES" - - if [ "$ALL_DONE" = true ]; then - echo "āœ… All devices completed (${ELAPSED}s elapsed)" - break - fi - fi - - echo "šŸ“Š Status: $STATUS (${ELAPSED}s elapsed)" - - # Check for completion - if [[ "$STATUS" =~ ^(done|failed|error|passed|completed)$ ]]; then - echo "āœ… Build completed with status: $STATUS" - break - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - # Get final results - FINAL_RESULT=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/android/builds/$BUILD_ID") - - echo "šŸ“‹ Final results:" - echo "$FINAL_RESULT" | jq . - - # Validate and check results - if echo "$FINAL_RESULT" | jq -e .devices > /dev/null 2>&1; then - BUILD_STATUS=$(echo "$FINAL_RESULT" | yq eval .status) - if [ "$BUILD_STATUS" != "passed" ]; then - echo "āŒ Tests failed with status: $BUILD_STATUS" - - FAILED_DEVICES=$(echo "$FINAL_RESULT" | yq eval '.devices[] | select(.sessions[].status != "passed") | .device') - if [ -n "$FAILED_DEVICES" ]; then - echo "Failed on devices: $FAILED_DEVICES" - fi - exit 1 - else - echo "šŸŽ‰ All tests passed successfully!" - echo "Dashboard URL: https://app-automate.browserstack.com/dashboard/v2/builds/$BUILD_ID" - fi - else - echo "āš ļø Could not parse final results" - exit 1 - fi - - - name: Add to GitHub Actions Summary - if: always() - run: | - BUILD_ID="${{ steps.test.outputs.build_id }}" - echo "### šŸ¤– BrowserStack Android Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ job.status }}" = "success" ]; then - echo "āœ… **Status:** Passed" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed" >> $GITHUB_STEP_SUMMARY - fi - - if [ -n "$BUILD_ID" ]; then - echo "- **Build ID:** $BUILD_ID" >> $GITHUB_STEP_SUMMARY - echo "- **Dashboard:** [View Results](https://app-automate.browserstack.com/dashboard/v2/builds/$BUILD_ID/)" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - browserstack-ios: - name: BrowserStack iOS Testing - runs-on: macos-latest - needs: [build-ios] - if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - timeout-minutes: 150 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env - - - name: Get dependencies - working-directory: flutter_app - run: flutter pub get - - - name: Install CocoaPods - working-directory: flutter_app/ios - run: pod install || pod install --repo-update - - - name: Download iOS IPA - uses: actions/download-artifact@v4 - with: - name: flutter-ios-ipa-${{ github.run_number }} - path: flutter_app/build/ios/ipa/ - - - name: Get BrowserStack build info (iOS) - id: build-info-ios - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (iOS)' - title-max-length: '90' - commit-max-length: '130' - - - name: Seed, build, upload, and execute BrowserStack iOS tests - id: test - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "šŸ“ Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "āœ… Document inserted successfully with title: $TASK_TITLE" - else - echo "āŒ Failed to insert document. HTTP status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Build iOS integration test package with seeded task title - cd flutter_app - echo "🧪 Building iOS integration test package with task title: $TASK_TITLE" - - flutter build ios integration_test/app_test.dart --release --no-codesign \ - --dart-define=INTEGRATION_TEST_MODE=true \ - --dart-define="TASK_TO_FIND=$TASK_TITLE" - - # Create iOS test package - cd .. - output="../build/ios_integration" - product="build/ios_integration/Build/Products" - - pushd flutter_app/ios - xcodebuild -workspace Runner.xcworkspace \ - -scheme Runner \ - -config Flutter/Release.xcconfig \ - -derivedDataPath $output \ - -sdk iphoneos \ - build-for-testing \ - CODE_SIGNING_ALLOWED=NO - popd - - pushd flutter_app/$product - XCTESTRUN_FILE=$(find . -name "*.xctestrun" -type f | head -1) - if [ -z "$XCTESTRUN_FILE" ]; then - echo "āŒ No .xctestrun file found" - exit 1 - fi - echo "šŸ“¦ Found xctestrun file: $XCTESTRUN_FILE" - zip -r "ios_test_package.zip" "Release-iphoneos" "$XCTESTRUN_FILE" - popd - - # Upload iOS test package to BrowserStack - echo "šŸ“¤ Uploading iOS test package to BrowserStack..." - TEST_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/ios/test-package" \ - -F "file=@flutter_app/build/ios_integration/Build/Products/ios_test_package.zip") - - echo "Upload response: $TEST_RESPONSE" - TEST_PACKAGE_URL=$(echo "$TEST_RESPONSE" | yq eval -p=json .test_package_url) - - if [ "$TEST_PACKAGE_URL" = "null" ] || [ -z "$TEST_PACKAGE_URL" ]; then - echo "āŒ Failed to upload iOS test package" - echo "Response: $TEST_RESPONSE" - exit 1 - fi - echo "āœ… iOS test package uploaded: $TEST_PACKAGE_URL" - - # Execute tests on BrowserStack - echo "šŸš€ Executing tests on BrowserStack..." - - # Create test execution request - # NOTE: Flutter testing framework requires iOS 15+ due to _backtrace_async symbol - # See: https://developer.apple.com/documentation/os/backtrace_async - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.flutter.ios.devices' .github/browserstack-devices.yml) - - BUILD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/ios/build" \ - -H "Content-Type: application/json" \ - -d "{ - \"testPackage\": \"$TEST_PACKAGE_URL\", - \"devices\": $DEVICES, - \"project\": \"${{ steps.build-info-ios.outputs.project-name }}\", - \"buildName\": \"${{ steps.build-info-ios.outputs.build-name }}\", - \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", - \"deviceLogs\": true, - \"networkLogs\": true, - \"autoGrantPermissions\": true - }") - - echo "BrowserStack API Response:" - echo "$BUILD_RESPONSE" - - BUILD_ID=$(echo "$BUILD_RESPONSE" | yq eval .build_id) - echo "build_id=$BUILD_ID" >> "$GITHUB_OUTPUT" - - # Check if BUILD_ID is null or empty - if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then - echo "Error: Failed to create BrowserStack build" - echo "Response: $BUILD_RESPONSE" - exit 1 - fi - - echo "Build started with ID: $BUILD_ID" - - # Wait for tests to complete - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=30 # Check every 30 seconds - ELAPSED=0 - - echo "ā³ Waiting for test execution to complete..." - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/ios/builds/$BUILD_ID") - - STATUS=$(echo "$RESPONSE" | yq eval .status) - - if [ "$STATUS" = "null" ] || [ -z "$STATUS" ]; then - echo "āš ļø API error, retrying... (${ELAPSED}s elapsed)" - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - continue - fi - - echo "šŸ“Š Build Status: $STATUS (${ELAPSED}s elapsed)" - - # Check device statuses - if all devices are done, we're done - DEVICE_STATUSES=$(echo "$RESPONSE" | yq eval '.devices[]?.sessions[]?.status' 2>/dev/null) - if [ -n "$DEVICE_STATUSES" ]; then - ALL_DONE=true - while IFS= read -r dev_status; do - if [[ ! "$dev_status" =~ ^(passed|failed|error|done)$ ]]; then - ALL_DONE=false - break - fi - done <<< "$DEVICE_STATUSES" - - if [ "$ALL_DONE" = true ]; then - echo "āœ… All devices completed (${ELAPSED}s elapsed)" - break - fi - fi - - # Check for build-level completion as fallback - if [[ "$STATUS" =~ ^(done|failed|error|passed|completed)$ ]]; then - echo "āœ… Build completed with status: $STATUS" - break - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - # Get final results - FINAL_RESULT=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/ios/builds/$BUILD_ID") - - echo "šŸ“‹ Final results:" - echo "$FINAL_RESULT" | jq . - - # Validate and check results - if echo "$FINAL_RESULT" | jq -e .devices > /dev/null 2>&1; then - BUILD_STATUS=$(echo "$FINAL_RESULT" | yq eval .status) - if [ "$BUILD_STATUS" != "passed" ]; then - echo "āŒ Tests failed with status: $BUILD_STATUS" - - FAILED_DEVICES=$(echo "$FINAL_RESULT" | yq eval '.devices[] | select(.sessions[].status != "passed") | .device') - if [ -n "$FAILED_DEVICES" ]; then - echo "Failed on devices: $FAILED_DEVICES" - fi - exit 1 - else - echo "šŸŽ‰ All tests passed successfully!" - echo "Dashboard URL: https://app-automate.browserstack.com/dashboard/v2/builds/$BUILD_ID" - fi - else - echo "āš ļø Could not parse final results" - exit 1 - fi - - - name: Add to GitHub Actions Summary - if: always() - run: | - BUILD_ID="${{ steps.test.outputs.build_id }}" - echo "### šŸŽ BrowserStack iOS Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ job.status }}" = "success" ]; then - echo "āœ… **Status:** Passed" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed" >> $GITHUB_STEP_SUMMARY - fi - - if [ -n "$BUILD_ID" ]; then - echo "- **Build ID:** $BUILD_ID" >> $GITHUB_STEP_SUMMARY - echo "- **Dashboard:** [View Results](https://app-automate.browserstack.com/dashboard/v2/builds/$BUILD_ID/)" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - summary: - name: Test Summary - runs-on: ubuntu-latest - needs: - [ - lint, - unit-tests, - build-android, - build-ios, - build-web, - build-macos, - build-windows, - browserstack-android, - browserstack-ios, - ] - if: always() - steps: - - name: Create Overall Summary - run: | - echo "## šŸ“Š BrowserStack Test Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check overall status - if [ "${{ needs.browserstack-android.result }}" = "success" ] && [ "${{ needs.browserstack-ios.result }}" = "success" ]; then - echo "šŸŽ‰ **Overall Status:** All tests passed!" >> $GITHUB_STEP_SUMMARY - elif [ "${{ needs.browserstack-android.result }}" = "skipped" ] || [ "${{ needs.browserstack-ios.result }}" = "skipped" ]; then - echo "āš ļø **Overall Status:** Some tests were skipped" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Overall Status:** Some tests failed" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Results:" >> $GITHUB_STEP_SUMMARY - echo "- **Android:** ${{ needs.browserstack-android.result }}" >> $GITHUB_STEP_SUMMARY - echo "- **iOS:** ${{ needs.browserstack-ios.result }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/java-server-ci.yml b/.github/workflows/java-server-ci.yml deleted file mode 100644 index 6569b1805..000000000 --- a/.github/workflows/java-server-ci.yml +++ /dev/null @@ -1,349 +0,0 @@ -# -# .github/workflows/java-server-ci.yml -# Workflow for building and testing java-server with BrowserStack integration -# ---- -name: Java Server CI - -on: - pull_request: - paths: - - "java-server/**" - - ".github/workflows/java-server-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint (ubuntu-latest) - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - - - name: Create .env file (root) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Run linter (PMD + SpotBugs) - working-directory: java-server - run: ./gradlew pmdMain pmdTest spotbugsMain - - build: - name: Build (ubuntu-latest) - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 20 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - - - name: Create .env file (root) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Build Spring Boot JAR - working-directory: java-server - env: - DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} - DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} - DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} - DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} - DITTO_ENABLE_CLOUD_SYNC: true - run: ./gradlew bootJar -x test - - - name: Upload JAR artifacts - uses: actions/upload-artifact@v4 - with: - name: java-server-jar-${{ github.run_number }} - path: java-server/build/libs/*.jar - retention-days: 1 - - browserstack-test: - name: BrowserStack Device Testing (macos-latest) - runs-on: macos-latest - needs: [build] - if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - timeout-minutes: 150 - outputs: - build_id: ${{ steps.test.outputs.build_id }} - - steps: - - uses: actions/checkout@v4 - - - name: Download JAR artifacts - uses: actions/download-artifact@v4 - with: - name: java-server-jar-${{ github.run_number }} - path: java-server/build/libs/ - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - - - name: Create .env file (root) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Install BrowserStack Local binary - run: | - curl -fL -O "https://www.browserstack.com/browserstack-local/BrowserStackLocal-darwin-x64.zip" - unzip BrowserStackLocal-darwin-x64.zip - chmod +x BrowserStackLocal - - - name: Start BrowserStack Local tunnel - run: | - echo "Starting BrowserStack Local tunnel..." - nohup ./BrowserStackLocal --key "${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - --daemon-mode > browserstack-local.log 2>&1 & - - # Wait for tunnel to establish - TIMEOUT=60 - ELAPSED=0 - - while [ $ELAPSED -lt $TIMEOUT ]; do - if [ -f browserstack-local.log ] && grep -q "You can now access your local server(s) in our remote browser" browserstack-local.log; then - echo "BrowserStack Local tunnel established" - exit 0 - fi - if [ -f browserstack-local.log ] && grep -q -i "error\|failed\|unable" browserstack-local.log; then - echo "BrowserStack Local tunnel failed:" - cat browserstack-local.log - exit 1 - fi - sleep 5 - ELAPSED=$((ELAPSED + 5)) - done - - echo "BrowserStack Local tunnel timeout after ${TIMEOUT}s" - [ -f browserstack-local.log ] && cat browserstack-local.log - exit 1 - - - name: Get BrowserStack build info - id: build-info - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: " (Java)" - title-max-length: "90" - commit-max-length: "130" - - - name: Create BrowserStack config - working-directory: java-server - run: | - # Load platforms from centralized config and convert to YAML format - PLATFORMS=$(yq eval '.["java-spring"].platforms[] | " - os: \(.os)\n osVersion: \"\(.osVersion)\"\n browserName: \(.browserName)\n browserVersion: \(.browserVersion)"' ../.github/browserstack-devices.yml) - - cat > browserstack.yml << EOF - userName: ${{ secrets.BROWSERSTACK_USERNAME }} - accessKey: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - projectName: ${{ steps.build-info.outputs.project-name }} - buildName: ${{ steps.build-info.outputs.build-name }} - platforms: - $PLATFORMS - browserstackLocal: true - debug: true - video: true - parallelsPerPlatform: 1 - EOF - - - name: Start Spring Boot app in background - working-directory: java-server - env: - DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} - DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} - DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} - DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} - DITTO_ENABLE_CLOUD_SYNC: true - run: | - nohup java -jar build/libs/*.jar \ - --server.port=8080 \ - --spring.profiles.active=test > app.log 2>&1 & - echo $! > app.pid - - # Wait for application to be ready - echo "Waiting for Spring Boot app to start..." - for i in {1..30}; do - if curl -f http://localhost:8080 >/dev/null 2>&1; then - echo "Spring Boot app is ready" - break - fi - sleep 2 - done - - - name: Seed and execute Selenium tests on BrowserStack cloud browsers - # working-directory: java-server - id: test - uses: nick-fields/retry@v3 - env: - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - BROWSERSTACK_BUILD_NAME: "Java Spring Selenium Tests #${{ github.run_number }}" - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} - BROWSERSTACK_LOCAL: true - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Export as environment variable (for System.getenv()) - export DITTO_CLOUD_TASK_TITLE="$TASK_TITLE" - - # Run BrowserStack Selenium tests - cd java-spring - ./gradlew test --tests "*TaskVisibilityIntegrationTest.shouldPassWithExistingTask" \ - -DBROWSERSTACK_USERNAME="${{ secrets.BROWSERSTACK_USERNAME }}" \ - -DBROWSERSTACK_ACCESS_KEY="${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -DBROWSERSTACK_LOCAL=true \ - -DDITTO_CLOUD_TASK_TITLE="$TASK_TITLE" \ - --info - - # Query BrowserStack API to get the build ID - echo "Fetching build ID from BrowserStack..." - BUILDS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api.browserstack.com/automate/builds.json?limit=1") - - BUILD_ID=$(echo "$BUILDS_RESPONSE" | yq eval '.[0].hashed_id' -) - - if [ "$BUILD_ID" != "null" ] && [ -n "$BUILD_ID" ]; then - echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT - echo "āœ… Build ID: $BUILD_ID" - else - echo "āš ļø Could not retrieve build ID from BrowserStack API" - fi - - - name: Stop Spring Boot app - if: always() - working-directory: java-server - run: | - if [ -f app.pid ]; then - kill $(cat app.pid) || true - rm app.pid - fi - - - name: Stop BrowserStack Local tunnel - if: always() - run: pkill -f "BrowserStackLocal" || true - - - name: Upload test reports - if: always() - uses: actions/upload-artifact@v4 - with: - name: browserstack-test-reports-${{ github.run_number }} - path: java-server/build/reports/tests/ - retention-days: 1 - - - name: Upload app logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: spring-boot-logs-${{ github.run_number }} - path: | - java-server/app.log - browserstack-local.log - retention-days: 1 - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build, browserstack-test] - if: always() - - steps: - - name: Report Results - run: | - echo "## ā˜• Java Spring CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build.result }}" == "success" && \ - "${{ needs.browserstack-test.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| BrowserStack Tests | ${{ needs.browserstack-test.result == 'success' && 'āœ… Passed' || (needs.browserstack-test.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # BrowserStack link - if [[ "${{ needs.browserstack-test.result }}" != "skipped" ]] && [ -n "${{ needs.browserstack-test.outputs.build_id }}" ]; then - echo "### BrowserStack Session" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "šŸ”— [View Test Results](https://automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack-test.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Tested Browser:**" >> $GITHUB_STEP_SUMMARY - echo "- Chrome Latest (Windows 11)" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/javascript-tui-ci.yml b/.github/workflows/javascript-tui-ci.yml deleted file mode 100644 index f9bd12088..000000000 --- a/.github/workflows/javascript-tui-ci.yml +++ /dev/null @@ -1,142 +0,0 @@ -name: JavaScript TUI CI -on: - pull_request: - paths: - - 'javascript-tui/**' - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: javascript-tui/package-lock.json - - - name: Install dependencies - working-directory: javascript-tui - run: npm ci - - - name: Run lint check - working-directory: javascript-tui - run: npm run test:format - - build: - name: Build - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: javascript-tui/package-lock.json - - - name: Install dependencies - working-directory: javascript-tui - run: npm ci - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Build application - working-directory: javascript-tui - run: npm run build - - integration-test: - name: Integration Test - # Disabled: Ditto Cloud API key expired/invalid since 2026-03-16. See DEVX-759. - if: false - runs-on: ubuntu-latest - needs: [lint, build] - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: javascript-tui/package-lock.json - - - name: Install dependencies - working-directory: javascript-tui - run: npm ci - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Seed test document to Ditto Cloud - id: seed - uses: ./.github/actions/seed-ditto-document - with: - ditto-api-key: ${{ secrets.DITTO_API_KEY }} - ditto-api-url: ${{ secrets.DITTO_API_URL }} - app-name: 'javascript-tui' - - - name: Run integration test - working-directory: javascript-tui - env: - DITTO_CLOUD_TASK_TITLE: ${{ steps.seed.outputs.document-title }} - GITHUB_RUN_ID: ${{ github.run_id }} - GITHUB_RUN_NUMBER: ${{ github.run_number }} - run: npm run build && node tests/integration_test.js - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build, integration-test] - if: always() - - steps: - - name: Report Results - run: | - echo "## šŸ“œ JavaScript TUI CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build.result }}" == "success" && \ - "${{ needs.integration-test.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Integration Test | ${{ needs.integration-test.result == 'success' && 'āœ… Passed' || (needs.integration-test.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/javascript-web-ci.yml b/.github/workflows/javascript-web-ci.yml deleted file mode 100644 index ca417dad4..000000000 --- a/.github/workflows/javascript-web-ci.yml +++ /dev/null @@ -1,257 +0,0 @@ -name: JavaScript Web CI - -on: - pull_request: - paths: - - "javascript-web/**" - - ".github/workflows/javascript-web-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "npm" - cache-dependency-path: javascript-web/package-lock.json - - - name: Install dependencies - working-directory: javascript-web - run: npm ci - - - name: Run linter - working-directory: javascript-web - run: npm run lint - - build: - name: Build - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "npm" - cache-dependency-path: javascript-web/package-lock.json - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Install dependencies - working-directory: javascript-web - run: npm ci - - - name: Build application - working-directory: javascript-web - run: npm run build - - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: javascript-web-dist - path: javascript-web/dist/ - retention-days: 1 - - browserstack-web: - name: BrowserStack Tests - # Disabled: Ditto Cloud API key expired/invalid since 2026-03-18. See DEVX-759. - if: false - runs-on: ubuntu-latest - needs: build - timeout-minutes: 150 - outputs: - build_id: ${{ steps.test.outputs.build_id }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - - - name: Download build artifact - uses: actions/download-artifact@v4 - with: - name: javascript-web-dist - path: javascript-web/dist - - - name: Install http-server - run: npm install -g http-server - - - name: Start web server - working-directory: javascript-web - run: | - nohup http-server dist -p 3000 -c-1 --cors > server.log 2>&1 & - sleep 5 - curl -f http://localhost:3000/ || (echo "Server failed to start" && cat server.log && exit 1) - echo "Web server started on http://localhost:3000" - - - name: Install BrowserStack Local binary - run: | - wget "https://www.browserstack.com/browserstack-local/BrowserStackLocal-linux-x64.zip" - unzip BrowserStackLocal-linux-x64.zip - chmod +x BrowserStackLocal - - # Create config file with restricted permissions - echo "key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}" > browserstack.yml - chmod 600 browserstack.yml - - nohup ./BrowserStackLocal --config-file ./browserstack.yml --daemon start & - sleep 20 - echo "BrowserStack Local tunnel established" - - - name: Generate BrowserStack names - id: build-info - uses: ./.github/actions/generate-browserstack-names - - - name: Make test script executable - run: chmod +x .github/scripts/browserstack-test.py - - - name: Seed and execute tests on BrowserStack - id: test - uses: nick-fields/retry@v3 - env: - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - BROWSERSTACK_PROJECT: ${{ steps.build-info.outputs.project-name }} - BROWSERSTACK_BUILD_NAME: ${{ steps.build-info.outputs.build-name }} - GITHUB_RUN_NUMBER: ${{ github.run_number }} - GITHUB_RUN_ID: ${{ github.run_id }} - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_javascript-web_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_javascript-web_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_javascript-web_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Set environment variable for Python script - export DITTO_CLOUD_TASK_TITLE="$TASK_TITLE" - - # Install Python dependencies - pip3 install selenium pyyaml - - # Run the test script - python3 .github/scripts/browserstack-test.py - - # Query BrowserStack API to get the build ID - echo "Fetching build ID from BrowserStack..." - BUILDS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api.browserstack.com/automate/builds.json?limit=1") - - BUILD_ID=$(echo "$BUILDS_RESPONSE" | yq eval '.[0].hashed_id' -) - - if [ "$BUILD_ID" != "null" ] && [ -n "$BUILD_ID" ]; then - echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT - echo "āœ… Build ID: $BUILD_ID" - else - echo "āš ļø Could not retrieve build ID from BrowserStack API" - fi - - - name: Stop BrowserStack Local tunnel - if: always() - run: ./BrowserStackLocal --key "${{ secrets.BROWSERSTACK_ACCESS_KEY }}" --daemon stop || true - - - name: Upload test artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: | - javascript-web/dist/ - javascript-web/server.log - *screenshot*.png - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build, browserstack-web] - if: always() - - steps: - - name: Report Results - run: | - echo "## 🌐 JavaScript Web CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build.result }}" == "success" && \ - "${{ needs.browserstack-web.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| BrowserStack Tests | ${{ needs.browserstack-web.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # BrowserStack info - if [ -n "${{ needs.browserstack-web.outputs.build_id }}" ]; then - echo "### BrowserStack Session" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "šŸ”— [View Test Results](https://automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack-web.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Tested Browsers:**" >> $GITHUB_STEP_SUMMARY - echo "- Chrome 120.0 (Windows 11)" >> $GITHUB_STEP_SUMMARY - echo "- Firefox 121.0 (Windows 11)" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/kotlin-multiplatform-ci.yml b/.github/workflows/kotlin-multiplatform-ci.yml deleted file mode 100644 index 877299007..000000000 --- a/.github/workflows/kotlin-multiplatform-ci.yml +++ /dev/null @@ -1,557 +0,0 @@ -name: Kotlin Multiplatform CI - -on: - pull_request: - paths: - - "kotlin-multiplatform/**" - - ".github/workflows/kotlin-multiplatform-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - -jobs: - lint: - name: Lint (ubuntu-latest) - runs-on: ubuntu-latest - timeout-minutes: 15 - defaults: - run: - working-directory: kotlin-multiplatform - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'gradle' - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - kotlin-multiplatform/.gradle - key: gradle-${{ runner.os }}-${{ hashFiles('kotlin-multiplatform/gradle/wrapper/gradle-wrapper.properties', 'kotlin-multiplatform/**/*.gradle*', 'kotlin-multiplatform/gradle/libs.versions.toml') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Create test .env file - run: | - echo "DITTO_APP_ID=test_app_id" > ../.env - echo "DITTO_PLAYGROUND_TOKEN=test_token" >> ../.env - echo "DITTO_AUTH_URL=https://test.com" >> ../.env - echo "DITTO_WEBSOCKET_URL=wss://test.com" >> ../.env - - - name: Make gradlew executable - run: chmod +x gradlew - - - name: Run Detekt lint - run: ./gradlew detekt --stacktrace - - - name: Upload Detekt reports - uses: actions/upload-artifact@v4 - if: always() - with: - name: detekt-reports - path: kotlin-multiplatform/composeApp/build/reports/detekt/ - - build-android: - name: Build Android (ubuntu-latest) - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: lint - defaults: - run: - working-directory: kotlin-multiplatform - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'gradle' - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - kotlin-multiplatform/.gradle - key: gradle-${{ runner.os }}-${{ hashFiles('kotlin-multiplatform/gradle/wrapper/gradle-wrapper.properties', 'kotlin-multiplatform/**/*.gradle*', 'kotlin-multiplatform/gradle/libs.versions.toml') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Create production .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > ../.env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> ../.env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> ../.env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> ../.env - - - name: Make gradlew executable - run: chmod +x gradlew - - - name: Build Android APKs - run: ./gradlew :composeApp:assembleDebug :composeApp:assembleDebugAndroidTest --stacktrace - - - name: Upload APK artifacts - uses: actions/upload-artifact@v4 - with: - name: android-kmp-apks-${{ github.run_number }} - path: | - kotlin-multiplatform/composeApp/build/outputs/apk/debug/composeApp-debug.apk - kotlin-multiplatform/composeApp/build/outputs/apk/androidTest/debug/composeApp-debug-androidTest.apk - retention-days: 1 - - build-ios: - name: Build iOS (macos-latest) - runs-on: macos-latest - timeout-minutes: 30 - needs: lint - outputs: - ios-build-success: ${{ steps.build-status.outputs.success }} - defaults: - run: - working-directory: kotlin-multiplatform - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'gradle' - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: 'latest-stable' - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - kotlin-multiplatform/.gradle - key: gradle-${{ runner.os }}-${{ hashFiles('kotlin-multiplatform/gradle/wrapper/gradle-wrapper.properties', 'kotlin-multiplatform/**/*.gradle*', 'kotlin-multiplatform/gradle/libs.versions.toml') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Write production .env - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > ../.env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> ../.env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> ../.env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> ../.env - - - name: Make gradlew executable - run: chmod +x gradlew - - - name: Build KMP iOS frameworks (sim + device) - run: | - ./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64 --stacktrace - ./gradlew :composeApp:linkDebugFrameworkIosArm64 --stacktrace - - # ---- iOS app build without archive/signing (more reliable on CI) ---- - - name: Prepare Xcode container (project/workspace + CocoaPods) - run: | - set -euo pipefail - if [ -f "iosApp/Podfile" ]; then - echo "Podfile found → installing pods" - sudo gem install cocoapods --no-document - cd iosApp - pod repo update - pod install - cd .. - fi - - if [ -f "iosApp/iosApp.xcworkspace" ]; then - echo "XCWORKSPACE=1" >> $GITHUB_ENV - CONTAINER="iosApp/iosApp.xcworkspace" - CONTAINER_FLAG="-workspace" - else - echo "XCWORKSPACE=0" >> $GITHUB_ENV - CONTAINER="iosApp/iosApp.xcodeproj" - CONTAINER_FLAG="-project" - fi - - echo "CONTAINER=$CONTAINER" >> $GITHUB_ENV - echo "CONTAINER_FLAG=$CONTAINER_FLAG" >> $GITHUB_ENV - - echo "Available schemes:" - xcodebuild -list $CONTAINER_FLAG "$CONTAINER" - - - name: Build device .app (no signing) - run: | - set -euo pipefail - DERIVED="$PWD/build/DerivedData" - PRODUCTS="$DERIVED/Build/Products" - - xcodebuild \ - $CONTAINER_FLAG "$CONTAINER" \ - -scheme iosApp \ - -configuration Debug \ - -sdk iphoneos \ - -derivedDataPath "$DERIVED" \ - CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \ - ENABLE_BITCODE=NO \ - ONLY_ACTIVE_ARCH=NO \ - build - - echo "Searching for device .app…" - APP_DIR=$(find "$PRODUCTS/Debug-iphoneos" -maxdepth 1 -type d -name "*.app" | head -1 || true) - if [ -z "${APP_DIR:-}" ]; then - echo "āŒ Could not locate Debug-iphoneos/*.app. Dumping tree for debugging:" - find "$PRODUCTS" -maxdepth 3 -print - exit 1 - fi - echo "APP_DIR=$APP_DIR" >> $GITHUB_ENV - echo "āœ… Found app: $APP_DIR" - - - name: Create unsigned .ipa for BrowserStack - run: | - set -euo pipefail - mkdir -p build/Payload - cp -R "$APP_DIR" build/Payload/ - (cd build && zip -qry iosApp-unsigned.ipa Payload && rm -rf Payload) - test -f build/iosApp-unsigned.ipa || (echo "āŒ IPA not created" && exit 1) - ls -la build/iosApp-unsigned.ipa - - - name: Upload iOS IPA Artifact - uses: actions/upload-artifact@v4 - with: - name: ios-kmp-ipa-${{ github.run_number }} - path: kotlin-multiplatform/build/iosApp-unsigned.ipa - retention-days: 1 - - - name: Set build status - id: build-status - run: echo "success=true" >> $GITHUB_OUTPUT - - build-desktop: - name: Build Desktop (ubuntu-latest) - runs-on: ubuntu-latest - timeout-minutes: 20 - defaults: - run: - working-directory: kotlin-multiplatform - - steps: - - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'gradle' - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - kotlin-multiplatform/.gradle - key: gradle-${{ runner.os }}-${{ hashFiles('kotlin-multiplatform/gradle/wrapper/gradle-wrapper.properties', 'kotlin-multiplatform/**/*.gradle*', 'kotlin-multiplatform/gradle/libs.versions.toml') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Create test .env file - run: | - echo "DITTO_APP_ID=test_app_id" > ../.env - echo "DITTO_PLAYGROUND_TOKEN=test_token" >> ../.env - echo "DITTO_AUTH_URL=https://test.com" >> ../.env - echo "DITTO_WEBSOCKET_URL=wss://test.com" >> ../.env - - - name: Make gradlew executable - run: chmod +x gradlew - - - name: Build Desktop application - run: ./gradlew :composeApp:packageDistributionForCurrentOS --stacktrace - - - name: Upload Desktop build outputs - uses: actions/upload-artifact@v4 - if: always() - with: - name: desktop-build-outputs - path: | - kotlin-multiplatform/composeApp/build/compose/binaries/main/ - kotlin-multiplatform/composeApp/build/reports/tests/desktopTest/ - - browserstack-android: - name: BrowserStack Android Testing - runs-on: ubuntu-latest - needs: [build-android] - if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - timeout-minutes: 150 - outputs: - build_id: ${{ steps.test.outputs.build_id }} - - steps: - - uses: actions/checkout@v4 - - - name: Download Android APK artifacts - uses: actions/download-artifact@v4 - with: - name: android-kmp-apks-${{ github.run_number }} - path: kotlin-multiplatform/composeApp/build/outputs/apk/ - - - name: Upload APKs to BrowserStack - id: upload - run: | - CREDS="${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" - - # Upload app APK - echo "Uploading app APK to BrowserStack..." - APP_RESPONSE=$(curl -u "$CREDS" \ - -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/app" \ - -F "file=@kotlin-multiplatform/composeApp/build/outputs/apk/debug/composeApp-debug.apk" \ - -F "custom_id=ditto-kotlin-multiplatform-app") - - APP_URL=$(echo "$APP_RESPONSE" | yq eval -p=json .app_url) - if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then - echo "Error: Failed to upload app APK" - echo "Response: $APP_RESPONSE" - exit 1 - fi - echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT" - echo "App APK uploaded: $APP_URL" - - # Upload test APK - echo "Uploading test APK to BrowserStack..." - TEST_RESPONSE=$(curl -u "$CREDS" \ - -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite" \ - -F "file=@kotlin-multiplatform/composeApp/build/outputs/apk/androidTest/debug/composeApp-debug-androidTest.apk" \ - -F "custom_id=ditto-kotlin-multiplatform-test") - - TEST_URL=$(echo "$TEST_RESPONSE" | yq eval -p=json .test_suite_url) - if [ "$TEST_URL" = "null" ] || [ -z "$TEST_URL" ]; then - echo "Error: Failed to upload test APK" - echo "Response: $TEST_RESPONSE" - exit 1 - fi - echo "test_url=$TEST_URL" >> "$GITHUB_OUTPUT" - echo "Test APK uploaded: $TEST_URL" - - - name: Get BrowserStack build info - id: build-info - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (Android)' - - - name: Seed and execute tests on BrowserStack - id: test - uses: nick-fields/retry@v3 - env: - BUILD_NAME: ${{ steps.build-info.outputs.build-name }} - PROJECT_NAME: ${{ steps.build-info.outputs.project-name }} - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_kotlin-multiplatform_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_kotlin-multiplatform_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_kotlin-multiplatform_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.["kotlin-multiplatform"].android.devices' .github/browserstack-devices.yml) - - # Use jq to properly escape the build name for JSON - BUILD_NAME_JSON=$(echo "$BUILD_NAME" | jq -Rs .) - PROJECT_NAME_JSON=$(echo "$PROJECT_NAME" | jq -Rs .) - - BUILD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/build" \ - -H "Content-Type: application/json" \ - -d "{ - \"app\": \"${{ steps.upload.outputs.app_url }}\", - \"testSuite\": \"${{ steps.upload.outputs.test_url }}\", - \"devices\": $DEVICES, - \"project\": $PROJECT_NAME_JSON, - \"buildName\": $BUILD_NAME_JSON, - \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", - \"deviceLogs\": true, - \"video\": true, - \"networkLogs\": true, - \"autoGrantPermissions\": true, - \"acceptInsecureCerts\": true, - \"enableWebsocketTunneling\": true, - \"instrumentationOptions\": { - \"DITTO_CLOUD_TASK_TITLE\": \"$TASK_TITLE\" - } - }") - - echo "BrowserStack API Response:" - echo "$BUILD_RESPONSE" - - BUILD_ID=$(echo "$BUILD_RESPONSE" | yq eval .build_id) - - # Check if BUILD_ID is null or empty - if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then - echo "Error: Failed to create BrowserStack build" - echo "Response: $BUILD_RESPONSE" - exit 1 - fi - - echo "Build started with ID: $BUILD_ID" - echo "build_id=$BUILD_ID" >> "$GITHUB_OUTPUT" - - # Wait for BrowserStack tests to complete - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=30 # Check every 30 seconds - ELAPSED=0 - - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - BUILD_STATUS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/$BUILD_ID") - - BUILD_STATUS=$(echo "$BUILD_STATUS_RESPONSE" | yq eval .status) - - # Check for API errors - if [ "$BUILD_STATUS" = "null" ] || [ -z "$BUILD_STATUS" ]; then - echo "Error getting build status. Response: $BUILD_STATUS_RESPONSE" - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - continue - fi - - echo "Build status: $BUILD_STATUS (elapsed: ${ELAPSED}s)" - - # Check for completion states - if [ "$BUILD_STATUS" = "done" ] || [ "$BUILD_STATUS" = "failed" ] || [ "$BUILD_STATUS" = "error" ] || [ "$BUILD_STATUS" = "passed" ] || [ "$BUILD_STATUS" = "completed" ]; then - echo "Build completed with status: $BUILD_STATUS" - break - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - # Get final results - FINAL_RESULT=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/$BUILD_ID") - - echo "Final build result:" - echo "$FINAL_RESULT" | jq . - - # Check if we got valid results - if echo "$FINAL_RESULT" | jq -e .devices > /dev/null 2>&1; then - # Check if the overall build passed - BUILD_STATUS=$(echo "$FINAL_RESULT" | yq eval .status) - if [ "$BUILD_STATUS" != "passed" ]; then - echo "Build failed with status: $BUILD_STATUS" - - # Check each device for failures - FAILED_TESTS=$(echo "$FINAL_RESULT" | yq eval '.devices[] | select(.sessions[].status != "passed") | .device') - - if [ -n "$FAILED_TESTS" ]; then - echo "Tests failed on devices: $FAILED_TESTS" - fi - - exit 1 - else - echo "All tests passed successfully!" - fi - else - echo "Warning: Could not parse final results" - echo "Raw response: $FINAL_RESULT" - fi - - # TEMPORARILY DISABLED: BrowserStack iOS Testing (will be re-enabled later) - # FIXME: Accessibility tags were not working to detect labels on BrowserStack at time of commenting - # browserstack-ios job removed for now - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build-android, build-ios, build-desktop, browserstack-android] - if: always() - - steps: - - name: Check build results - run: | - echo "## Kotlin Multiplatform CI Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Target | Status |" >> $GITHUB_STEP_SUMMARY - echo "|--------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Android Build | ${{ needs.build-android.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| iOS Build | ${{ needs.build-ios.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Desktop Build | ${{ needs.build-desktop.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Android BrowserStack | ${{ needs.browserstack-android.result == 'success' && 'āœ… Passed' || (needs.browserstack-android.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| iOS BrowserStack | ā­ļø Skipped (Temporarily Disabled) |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check core build results - if [[ "${{ needs.lint.result }}" != "success" ]] || \ - [[ "${{ needs.build-android.result }}" != "success" ]] || \ - [[ "${{ needs.build-ios.result }}" != "success" ]] || \ - [[ "${{ needs.build-desktop.result }}" != "success" ]]; then - echo "āŒ Core builds failed" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "āœ… All core builds passed successfully!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Testing results summary - if [[ "${{ needs.browserstack-android.result }}" == "success" ]]; then - echo "āœ… Android BrowserStack testing passed. iOS BrowserStack testing temporarily disabled." >> $GITHUB_STEP_SUMMARY - else - echo "āš ļø Core builds passed but testing had issues. Check testing logs above." >> $GITHUB_STEP_SUMMARY - fi - fi - - # BrowserStack link - if [[ "${{ needs.browserstack-android.result }}" != "skipped" ]]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### BrowserStack Session" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "šŸ¤– [View Android Test Results](https://app-automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack-android.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/publisher-authorization.yml b/.github/workflows/publisher-authorization.yml new file mode 100644 index 000000000..8a0ed21ec --- /dev/null +++ b/.github/workflows/publisher-authorization.yml @@ -0,0 +1,28 @@ +name: Public mirror source + +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + - ready_for_review + +# This workflow only reads pull request metadata from the protected base branch. +# It must never check out or execute pull request content. +permissions: {} + +jobs: + publisher-authorization: + name: Publisher authorization + runs-on: ubuntu-latest + steps: + - name: Require Ditto SDK release account + shell: bash + env: + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + run: | + if [[ "${PR_AUTHOR}" != "ditto-sdk-releases" ]]; then + echo "::error::This repository is a read-only mirror. Only pull requests opened by Ditto's SDK release process are accepted." + exit 1 + fi diff --git a/.github/workflows/react-native-ci.yml b/.github/workflows/react-native-ci.yml deleted file mode 100644 index 79beb2448..000000000 --- a/.github/workflows/react-native-ci.yml +++ /dev/null @@ -1,649 +0,0 @@ -name: React Native CI - -on: - pull_request: - paths: - - "react-native/**" - - ".github/workflows/react-native-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "yarn" - cache-dependency-path: react-native/yarn.lock - - - name: Install dependencies - working-directory: react-native - run: yarn install --frozen-lockfile - - - name: Run linting - working-directory: react-native - run: yarn lint - - build-android: - name: Build Android - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 150 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "yarn" - cache-dependency-path: react-native/yarn.lock - - - name: Install dependencies - working-directory: react-native - run: yarn install --frozen-lockfile - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Create .env file (production credentials for builds) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - react-native/android/.gradle - key: gradle-${{ runner.os }}-${{ hashFiles('react-native/android/gradle/wrapper/gradle-wrapper.properties', 'react-native/android/**/*.gradle*') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Build Android APK - working-directory: react-native/android - # Use direct gradlew instead of yarn android to generate unsigned APK required by BrowserStack - # yarn android creates signed builds that don't work with BrowserStack real device testing - run: NODE_ENV=production ./gradlew assembleRelease --no-daemon --console=plain - - - name: Upload Android APK artifact - uses: actions/upload-artifact@v4 - with: - name: android-apk-${{ github.run_number }} - path: react-native/android/app/build/outputs/apk/release/app-release.apk - retention-days: 1 - - build-ios: - name: Build iOS - runs-on: macos-latest - needs: lint - timeout-minutes: 150 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "yarn" - cache-dependency-path: react-native/yarn.lock - - - name: Install dependencies - working-directory: react-native - run: yarn install --frozen-lockfile - - - name: Create .env file (production credentials for builds) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Cache CocoaPods dependencies - uses: actions/cache@v4 - with: - path: | - react-native/ios/Pods - ~/Library/Caches/CocoaPods - ~/.cocoapods - key: cocoapods-${{ runner.os }}-${{ hashFiles('react-native/ios/Podfile.lock') }} - restore-keys: | - cocoapods-${{ runner.os }}- - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "16.4" - - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - working-directory: react-native - - - name: Install Ruby gems - working-directory: react-native - run: bundle install - - - name: Install CocoaPods - working-directory: react-native/ios - run: bundle exec pod install - - - name: Build iOS Archive and IPA (for BrowserStack) - working-directory: react-native - run: | - echo "šŸŽ Building iOS device .ipa for BrowserStack..." - - # Use direct xcodebuild instead of yarn ios to generate unsigned IPA required by BrowserStack - # yarn ios creates signed builds for simulators that don't work with BrowserStack real device testing - # Build and archive iOS app for real device - xcodebuild -workspace ios/DittoReactNativeSampleApp.xcworkspace \ - -scheme DittoReactNativeSampleApp \ - -configuration Release \ - -destination 'generic/platform=iOS' \ - -archivePath ios/build/DittoReactNativeSampleApp.xcarchive \ - archive \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO - - echo "šŸ“¦ Creating unsigned .ipa for BrowserStack..." - - # Find the .app bundle from the archive - APP_BUNDLE_PATH=$(find ios/build/DittoReactNativeSampleApp.xcarchive/Products/Applications -maxdepth 1 -name "*.app" -type d | head -1) - - if [ -d "$APP_BUNDLE_PATH" ]; then - echo "āœ… iOS app bundle found: $APP_BUNDLE_PATH" - - # Create unsigned IPA: Payload/.app zipped as .ipa - mkdir -p ios/build/Payload - cp -R "$APP_BUNDLE_PATH" ios/build/Payload/ - (cd ios/build && zip -qry DittoReactNativeSampleApp-unsigned.ipa Payload && rm -rf Payload) - - if [ -f "ios/build/DittoReactNativeSampleApp-unsigned.ipa" ]; then - echo "āœ… Unsigned .ipa created successfully" - ls -la ios/build/DittoReactNativeSampleApp-unsigned.ipa - else - echo "āŒ Failed to create .ipa file" - exit 1 - fi - else - echo "āŒ iOS app bundle not found in archive" - exit 1 - fi - - - name: Upload iOS IPA artifact - uses: actions/upload-artifact@v4 - with: - name: ios-ipa-${{ github.run_number }} - path: react-native/ios/build/DittoReactNativeSampleApp-unsigned.ipa - retention-days: 1 - - browserstack-android: - name: Test Android - BrowserStack Maestro - # Disabled: Ditto Cloud API key expired/invalid since 2026-03-16. See DEVX-759. - if: false - runs-on: ubuntu-latest - needs: [build-android] - timeout-minutes: 150 - outputs: - build_id: ${{ steps.execute-tests.outputs.build_id }} - status: ${{ job.status }} - # TODO: BrowserStack Android Maestro API environment variables don't work (confirmed by BS staff) - # On their roadmap with estimate: Oct 2025. Will revert to setEnvVariables when ready. - # Using sed workaround to replace ${MAESTRO_DITTO_CLOUD_TASK_TITLE} with actual task name in test files - - steps: - - uses: actions/checkout@v4 - - - name: Download Android APK - uses: actions/download-artifact@v4 - with: - name: android-apk-${{ github.run_number }} - path: ./artifacts/ - - - name: Get BrowserStack build info (Android) - id: build-info-android - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (Android)' - title-max-length: '90' - commit-max-length: '130' - - - name: Seed, create test suite, upload, and execute Android tests on BrowserStack - id: execute-tests - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Upload APK to BrowserStack - echo "Uploading APK to BrowserStack..." - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/app" \ - -F "file=@./artifacts/app-release.apk" \ - -F "custom_id=ReactNative-Android-${{ github.run_id }}") - - APP_URL=$(echo $response | yq eval -p=json '.app_url') - if [ -z "$APP_URL" ] || [ "$APP_URL" = "null" ]; then - echo "Error: Failed to upload APK" - echo "Response: $response" - exit 1 - fi - echo "APK uploaded: $APP_URL" - - # Create Maestro test suite ZIP with seeded task title - cd react-native - rm -rf maestro-tests - mkdir maestro-tests - - # Replace MAESTRO_DITTO_CLOUD_TASK_TITLE with actual seeded task name - sed "s/\${MAESTRO_DITTO_CLOUD_TASK_TITLE}/$TASK_TITLE/g" \ - .maestro/flows/01-app-launch-and-seeded-tasks-android.yaml > maestro-tests/01-app-launch-and-seeded-tasks-android.yaml - - mkdir maestro-tests/flows - cp maestro-tests/01-app-launch-and-seeded-tasks-android.yaml maestro-tests/flows/ - zip -r maestro-tests.zip maestro-tests -x "*.DS_Store" - cd .. - - # Upload Maestro test suite to BrowserStack - echo "Uploading test suite to BrowserStack..." - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ - -F "file=@react-native/maestro-tests.zip" \ - -F "custom_id=ReactNative-Tests-${{ github.run_id }}") - TEST_SUITE_URL=$(echo "$response" | yq eval -p=json '.test_suite_url') - if [ -z "$TEST_SUITE_URL" ] || [ "$TEST_SUITE_URL" = "null" ]; then - echo "Error: Failed to upload test suite" - echo "Response: $response" - exit 1 - fi - echo "Test suite uploaded: $TEST_SUITE_URL" - - # Execute tests - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.["react-native"].android.devices' .github/browserstack-devices.yml) - echo "Loaded devices: $DEVICES" - - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/android/build" \ - -H "Content-Type: application/json" \ - -d "{ - \"app\": \"$APP_URL\", - \"testSuite\": \"$TEST_SUITE_URL\", - \"project\": \"${{ steps.build-info-android.outputs.project-name }}\", - \"buildName\": \"${{ steps.build-info-android.outputs.build-name }}\", - \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", - \"devices\": $DEVICES, - \"execute\": [\"01-app-launch-and-seeded-tasks-android.yaml\"], - \"deviceLogs\": true, - \"networkLogs\": true, - \"video\": true - }") - - echo "Execution response: $response" - build_id=$(echo $response | yq eval '.build_id') - - if [ "$build_id" = "null" ] || [ -z "$build_id" ]; then - echo "āŒ Failed to create BrowserStack build" - echo "Response: $response" - exit 1 - fi - - echo "BrowserStack Build ID: $build_id" - - # Wait for test completion and get results - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=10 # Check every 10 seconds - ELAPSED=0 - - echo "ā³ Waiting for test execution to complete..." - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - response=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id") - - status=$(echo $response | yq eval '.status') - echo "Test status: $status (${ELAPSED}s elapsed)" - - if [ "$status" = "passed" ] || [ "$status" = "failed" ]; then - echo "Tests completed with status: $status" - echo $response | jq '.' - - if [ "$status" = "failed" ]; then - echo "āŒ Android Maestro tests failed!" - exit 1 - else - echo "āœ… Android Maestro tests passed!" - fi - break - elif [ "$status" = "error" ]; then - echo "āŒ BrowserStack reported build error. Fetching session details..." - echo $response | jq '.' - echo $response | yq eval '.devices[]?.sessions[]?.id' 2>/dev/null | while read sid; do - if [ -n "$sid" ]; then - echo "Session $sid details:" - curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id/sessions/$sid" | jq '.' - fi - done - exit 1 - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - if [ $ELAPSED -ge $MAX_WAIT_TIME ]; then - echo "āŒ Tests timed out after ${MAX_WAIT_TIME}s (18 minutes)" - exit 1 - fi - - browserstack-ios: - name: Test iOS - BrowserStack Maestro - # Disabled: Ditto Cloud API key expired/invalid since 2026-03-16. See DEVX-759. - if: false - runs-on: ubuntu-latest - needs: [build-ios] - timeout-minutes: 150 - outputs: - build_id: ${{ steps.execute-tests.outputs.build_id }} - status: ${{ job.status }} - - steps: - - uses: actions/checkout@v4 - - - name: Download iOS IPA - uses: actions/download-artifact@v4 - with: - name: ios-ipa-${{ github.run_number }} - path: ./artifacts/ - - - name: Get BrowserStack build info (iOS) - id: build-info-ios - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (iOS)' - title-max-length: '90' - commit-max-length: '130' - - - name: Seed, upload, and execute iOS Maestro tests on BrowserStack - id: execute-tests - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Upload IPA to BrowserStack - echo "Uploading IPA to BrowserStack..." - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/app" \ - -F "file=@./artifacts/DittoReactNativeSampleApp-unsigned.ipa" \ - -F "custom_id=ReactNative-iOS-${{ github.run_id }}") - - APP_URL=$(echo $response | yq eval -p=json '.app_url') - if [ -z "$APP_URL" ] || [ "$APP_URL" = "null" ]; then - echo "Error: Failed to upload IPA" - echo "Response: $response" - exit 1 - fi - echo "IPA uploaded: $APP_URL" - - # Create Maestro test suite ZIP - cd react-native - rm -rf maestro-tests - mkdir maestro-tests - - # Copy iOS-specific test - cp .maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml maestro-tests/ - - # Create flows/ subfolder - mkdir maestro-tests/flows - cp .maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml maestro-tests/flows/ - - # Create ZIP - zip -r maestro-tests.zip maestro-tests -x "*.DS_Store" - cd .. - - # Upload Maestro test suite to BrowserStack - echo "Uploading test suite to BrowserStack..." - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ - -F "file=@react-native/maestro-tests.zip" \ - -F "custom_id=ReactNative-iOS-Tests-${{ github.run_id }}") - - TEST_SUITE_URL=$(echo $response | yq eval '.test_suite_url') - if [ -z "$TEST_SUITE_URL" ] || [ "$TEST_SUITE_URL" = "null" ]; then - echo "Error: Failed to upload test suite" - echo "Response: $response" - exit 1 - fi - echo "Test suite uploaded: $TEST_SUITE_URL" - - # Execute tests - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.["react-native"].ios.devices' .github/browserstack-devices.yml) - echo "Loaded devices: $DEVICES" - - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/ios/build" \ - -H "Content-Type: application/json" \ - -d "{ - \"app\": \"$APP_URL\", - \"testSuite\": \"$TEST_SUITE_URL\", - \"project\": \"${{ steps.build-info-ios.outputs.project-name }}\", - \"buildName\": \"${{ steps.build-info-ios.outputs.build-name }}\", - \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", - \"devices\": $DEVICES, - \"execute\": [\"01-app-launch-and-seeded-tasks-ios.yaml\"], - \"setEnvVariables\": { - \"MAESTRO_DITTO_CLOUD_TASK_TITLE\": \"$TASK_TITLE\" - }, - \"deviceLogs\": true, - \"networkLogs\": true, - \"video\": true - }") - - echo "Execution response: $response" - build_id=$(echo $response | yq eval '.build_id') - - if [ "$build_id" = "null" ] || [ -z "$build_id" ]; then - echo "āŒ Failed to create BrowserStack build" - echo "Response: $response" - exit 1 - fi - - echo "BrowserStack Build ID: $build_id" - - # Wait for test completion and get results - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=10 # Check every 10 seconds - ELAPSED=0 - - echo "ā³ Waiting for test execution to complete..." - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - response=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id") - - status=$(echo $response | yq eval '.status') - echo "Test status: $status (${ELAPSED}s elapsed)" - - if [ "$status" = "passed" ] || [ "$status" = "failed" ]; then - echo "Tests completed with status: $status" - echo $response | jq '.' - - if [ "$status" = "failed" ]; then - echo "āŒ iOS Maestro tests failed!" - exit 1 - else - echo "āœ… iOS Maestro tests passed!" - fi - break - elif [ "$status" = "error" ]; then - echo "āŒ BrowserStack reported build error. Fetching session details..." - echo $response | jq '.' - echo $response | yq eval '.devices[]?.sessions[]?.id' 2>/dev/null | while read sid; do - if [ -n "$sid" ]; then - echo "Session $sid details:" - curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id/sessions/$sid" | jq '.' - fi - done - exit 1 - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - if [ $ELAPSED -ge $MAX_WAIT_TIME ]; then - echo "āŒ Tests timed out after ${MAX_WAIT_TIME}s (18 minutes)" - exit 1 - fi - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [browserstack-android, browserstack-ios] - if: always() - - steps: - - name: Generate BrowserStack Summary - run: | - echo "## šŸ“± React Native - BrowserStack Maestro Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Android results - echo "### Android Tests" >> $GITHUB_STEP_SUMMARY - ANDROID_BUILD_ID="${{ needs.browserstack-android.outputs.build_id }}" - ANDROID_STATUS="${{ needs.browserstack-android.outputs.status }}" - - if [ -n "$ANDROID_BUILD_ID" ] && [ "$ANDROID_BUILD_ID" != "null" ]; then - if [ "$ANDROID_STATUS" = "success" ]; then - echo "āœ… **Status:** Passed" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "šŸ”— **[View Dashboard](https://app-automate.browserstack.com/dashboard/v2/builds/$ANDROID_BUILD_ID)**" >> $GITHUB_STEP_SUMMARY - echo "Build ID: \`$ANDROID_BUILD_ID\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed (No build created)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # iOS results - echo "### iOS Tests" >> $GITHUB_STEP_SUMMARY - IOS_BUILD_ID="${{ needs.browserstack-ios.outputs.build_id }}" - IOS_STATUS="${{ needs.browserstack-ios.outputs.status }}" - - if [ -n "$IOS_BUILD_ID" ] && [ "$IOS_BUILD_ID" != "null" ]; then - if [ "$IOS_STATUS" = "success" ]; then - echo "āœ… **Status:** Passed" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "šŸ”— **[View Dashboard](https://app-automate.browserstack.com/dashboard/v2/builds/$IOS_BUILD_ID)**" >> $GITHUB_STEP_SUMMARY - echo "Build ID: \`$IOS_BUILD_ID\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed (No build created)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # Summary - echo "---" >> $GITHUB_STEP_SUMMARY - if [ "$ANDROID_STATUS" = "success" ] && [ "$IOS_STATUS" = "success" ]; then - echo "āœ… **Overall Result:** All tests passed" >> $GITHUB_STEP_SUMMARY - else - echo "āš ļø **Overall Result:** Some tests failed" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/react-native-expo-ci.yml b/.github/workflows/react-native-expo-ci.yml deleted file mode 100644 index 05f503a10..000000000 --- a/.github/workflows/react-native-expo-ci.yml +++ /dev/null @@ -1,654 +0,0 @@ -name: React Native Expo CI - -on: - pull_request: - paths: - - "react-native-expo/**" - - ".github/workflows/react-native-expo-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "yarn" - cache-dependency-path: react-native-expo/yarn.lock - - - name: Install dependencies - working-directory: react-native-expo - run: yarn install --frozen-lockfile - - - name: Run linting - working-directory: react-native-expo - run: yarn lint - - build-android: - name: Build Android - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 150 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "yarn" - cache-dependency-path: react-native-expo/yarn.lock - - - name: Install dependencies - working-directory: react-native-expo - run: yarn install --frozen-lockfile - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Create .env file (production credentials for builds) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - react-native-expo/android/.gradle - key: gradle-expo-${{ runner.os }}-${{ hashFiles('react-native-expo/android/gradle/wrapper/gradle-wrapper.properties', 'react-native-expo/android/**/*.gradle*') }} - restore-keys: | - gradle-expo-${{ runner.os }}- - - - name: Prebuild Android project - working-directory: react-native-expo - run: npx expo prebuild --platform android - - - name: Build Android APK - working-directory: react-native-expo/android - # Use direct gradlew instead of yarn android to generate unsigned APK required by BrowserStack - # yarn android creates signed builds that don't work with BrowserStack real device testing - run: NODE_ENV=production ./gradlew assembleRelease --no-daemon --console=plain - - - name: Upload Android APK artifact - uses: actions/upload-artifact@v4 - with: - name: expo-android-apk-${{ github.run_number }} - path: react-native-expo/android/app/build/outputs/apk/release/app-release.apk - retention-days: 1 - - build-ios: - name: Build iOS - runs-on: macos-latest - needs: lint - timeout-minutes: 150 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "yarn" - cache-dependency-path: react-native-expo/yarn.lock - - - name: Install dependencies - working-directory: react-native-expo - run: yarn install --frozen-lockfile - - - name: Create .env file (production credentials for builds) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Cache CocoaPods dependencies - uses: actions/cache@v4 - with: - path: | - ~/Library/Caches/CocoaPods - ~/.cocoapods - key: cocoapods-expo-${{ runner.os }}-${{ hashFiles('react-native-expo/ios/Podfile.lock') }} - restore-keys: | - cocoapods-expo-${{ runner.os }}- - - - name: Prebuild iOS project - working-directory: react-native-expo - run: npx expo prebuild --platform ios - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - # Xcode 26.x ships Swift 6.2, required by expo-modules-core 55.x which - # uses `@MainActor` on protocol conformance (Swift 6.2 syntax). 16.4 - # ships Swift 6.0 and produces "unknown attribute 'MainActor'" errors. - xcode-version: "26.3" - - - name: Install CocoaPods - working-directory: react-native-expo/ios - run: pod install --repo-update - - - name: Build iOS Archive and IPA (for BrowserStack) - working-directory: react-native-expo - run: | - echo "šŸŽ Building iOS device .ipa for BrowserStack..." - - # Use direct xcodebuild instead of yarn ios to generate unsigned IPA required by BrowserStack - # yarn ios creates signed builds for simulators that don't work with BrowserStack real device testing - # Build and archive iOS app for real device - xcodebuild -workspace ios/reactnativeexpo.xcworkspace \ - -scheme reactnativeexpo \ - -configuration Release \ - -destination 'generic/platform=iOS' \ - -archivePath ios/build/reactnativeexpo.xcarchive \ - archive \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO - - echo "šŸ“¦ Creating unsigned .ipa for BrowserStack..." - - # Find the .app bundle from the archive - APP_BUNDLE_PATH=$(find ios/build/reactnativeexpo.xcarchive/Products/Applications -maxdepth 1 -name "*.app" -type d | head -1) - - if [ -d "$APP_BUNDLE_PATH" ]; then - echo "āœ… iOS app bundle found: $APP_BUNDLE_PATH" - - # Create unsigned IPA: Payload/.app zipped as .ipa - mkdir -p ios/build/Payload - cp -R "$APP_BUNDLE_PATH" ios/build/Payload/ - (cd ios/build && zip -qry reactnativeexpo-unsigned.ipa Payload && rm -rf Payload) - - if [ -f "ios/build/reactnativeexpo-unsigned.ipa" ]; then - echo "āœ… Unsigned .ipa created successfully" - ls -la ios/build/reactnativeexpo-unsigned.ipa - else - echo "āŒ Failed to create .ipa file" - exit 1 - fi - else - echo "āŒ iOS app bundle not found in archive" - exit 1 - fi - - - name: Upload iOS IPA artifact - uses: actions/upload-artifact@v4 - with: - name: expo-ios-ipa-${{ github.run_number }} - path: react-native-expo/ios/build/reactnativeexpo-unsigned.ipa - retention-days: 1 - - browserstack-android: - name: Test Android - BrowserStack Maestro - # Disabled: Ditto Cloud API key expired/invalid since 2026-03-16. See DEVX-759. - if: false - runs-on: ubuntu-latest - needs: [build-android] - timeout-minutes: 150 - outputs: - build_id: ${{ steps.execute-tests.outputs.build_id }} - status: ${{ job.status }} - # TODO: BrowserStack Android Maestro API environment variables don't work (confirmed by BS staff) - # On their roadmap with estimate: Oct 2025. Will revert to setEnvVariables when ready. - # Using sed workaround to replace ${MAESTRO_DITTO_CLOUD_TASK_TITLE} with actual task name in test files - - steps: - - uses: actions/checkout@v4 - - - name: Download Android APK - uses: actions/download-artifact@v4 - with: - name: expo-android-apk-${{ github.run_number }} - path: ./artifacts/ - - - name: Get BrowserStack build info (Android) - id: build-info-android - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (Android)' - title-max-length: '90' - commit-max-length: '130' - - - name: Seed, create test suite, upload, and execute Android Maestro tests - id: execute-tests - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_react-native-expo_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_react-native-expo_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_react-native-expo_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Upload APK to BrowserStack - echo "Uploading APK to BrowserStack..." - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/app" \ - -F "file=@./artifacts/app-release.apk" \ - -F "custom_id=ReactNative-Expo-Android-${{ github.run_id }}") - - APP_URL=$(echo $response | yq eval -p=json '.app_url') - if [ -z "$APP_URL" ] || [ "$APP_URL" = "null" ]; then - echo "Error: Failed to upload APK" - echo "Response: $response" - exit 1 - fi - echo "APK uploaded: $APP_URL" - - # Create Maestro test suite ZIP with sed replacement - cd react-native-expo - rm -rf maestro-tests - mkdir maestro-tests - - # Replace MAESTRO_DITTO_CLOUD_TASK_TITLE with actual seeded task name (BrowserStack Android API bug workaround) - sed "s/\${MAESTRO_DITTO_CLOUD_TASK_TITLE}/$TASK_TITLE/g" \ - .maestro/flows/01-app-launch-and-seeded-tasks-android.yaml > maestro-tests/01-app-launch-and-seeded-tasks-android.yaml - - # Also create flows/ subfolder for organization - mkdir maestro-tests/flows - cp maestro-tests/01-app-launch-and-seeded-tasks-android.yaml maestro-tests/flows/ - - # Create ZIP with proper structure - zip -r maestro-tests.zip maestro-tests -x "*.DS_Store" - cd .. - - # Upload Maestro test suite to BrowserStack - echo "Uploading test suite to BrowserStack..." - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ - -F "file=@react-native-expo/maestro-tests.zip" \ - -F "custom_id=ReactNative-Expo-Tests-${{ github.run_id }}") - - TEST_SUITE_URL=$(echo $response | yq eval -p=json '.test_suite_url') - if [ -z "$TEST_SUITE_URL" ] || [ "$TEST_SUITE_URL" = "null" ]; then - echo "Error: Failed to upload test suite" - echo "Response: $response" - exit 1 - fi - echo "Test suite uploaded: $TEST_SUITE_URL" - - # Execute tests - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.["react-native-expo"].android.devices' .github/browserstack-devices.yml) - echo "Loaded devices: $DEVICES" - - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/android/build" \ - -H "Content-Type: application/json" \ - -d "{ - \"app\": \"$APP_URL\", - \"testSuite\": \"$TEST_SUITE_URL\", - \"project\": \"${{ steps.build-info-android.outputs.project-name }}\", - \"buildName\": \"${{ steps.build-info-android.outputs.build-name }}\", - \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", - \"devices\": $DEVICES, - \"execute\": [\"01-app-launch-and-seeded-tasks-android.yaml\"], - \"deviceLogs\": true, - \"networkLogs\": true, - \"video\": true - }") - - echo "Execution response: $response" - build_id=$(echo $response | yq eval '.build_id') - - if [ "$build_id" = "null" ] || [ -z "$build_id" ]; then - echo "āŒ Failed to create BrowserStack build" - echo "Response: $response" - exit 1 - fi - - echo "BrowserStack Build ID: $build_id" - echo "build_id=$build_id" >> $GITHUB_OUTPUT - - # Wait for test completion and get results - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=10 # Check every 10 seconds - ELAPSED=0 - - echo "ā³ Waiting for test execution to complete..." - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - response=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id") - - status=$(echo $response | yq eval '.status') - echo "Test status: $status (${ELAPSED}s elapsed)" - - if [ "$status" = "passed" ] || [ "$status" = "failed" ]; then - echo "Tests completed with status: $status" - echo $response | jq '.' - - if [ "$status" = "failed" ]; then - echo "āŒ Android Maestro tests failed!" - exit 1 - else - echo "āœ… Android Maestro tests passed!" - fi - break - elif [ "$status" = "error" ]; then - echo "āŒ BrowserStack reported build error. Fetching session details..." - echo $response | jq '.' - echo $response | yq eval '.devices[]?.sessions[]?.id' 2>/dev/null | while read sid; do - if [ -n "$sid" ]; then - echo "Session $sid details:" - curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id/sessions/$sid" | jq '.' - fi - done - exit 1 - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - if [ $ELAPSED -ge $MAX_WAIT_TIME ]; then - echo "āŒ Tests timed out after ${MAX_WAIT_TIME}s (18 minutes)" - exit 1 - fi - - browserstack-ios: - name: Test iOS - BrowserStack Maestro - # Disabled: Ditto Cloud API key expired/invalid since 2026-03-16. See DEVX-759. - if: false - runs-on: ubuntu-latest - needs: [build-ios] - timeout-minutes: 150 - outputs: - build_id: ${{ steps.execute-tests.outputs.build_id }} - status: ${{ job.status }} - - steps: - - uses: actions/checkout@v4 - - - name: Download iOS IPA - uses: actions/download-artifact@v4 - with: - name: expo-ios-ipa-${{ github.run_number }} - path: ./artifacts/ - - - name: Get BrowserStack build info (iOS) - id: build-info-ios - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (iOS)' - title-max-length: '90' - commit-max-length: '130' - - - name: Seed, upload, and execute iOS Maestro tests on BrowserStack - id: execute-tests - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_react-native-expo_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_react-native-expo_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_react-native-expo_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Upload IPA to BrowserStack - echo "Uploading IPA to BrowserStack..." - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/app" \ - -F "file=@./artifacts/reactnativeexpo-unsigned.ipa" \ - -F "custom_id=ReactNative-Expo-iOS-${{ github.run_id }}") - - APP_URL=$(echo $response | yq eval -p=json '.app_url') - if [ -z "$APP_URL" ] || [ "$APP_URL" = "null" ]; then - echo "Error: Failed to upload IPA" - echo "Response: $response" - exit 1 - fi - echo "IPA uploaded: $APP_URL" - - # Create Maestro test suite ZIP - cd react-native-expo - rm -rf maestro-tests - mkdir maestro-tests - - # Copy only iOS-specific tests to root level - cp .maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml maestro-tests/ - - # Also create flows/ subfolder for organization - mkdir maestro-tests/flows - cp .maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml maestro-tests/flows/ - - # Create ZIP with proper structure - zip -r maestro-tests.zip maestro-tests -x "*.DS_Store" - cd .. - - # Upload Maestro test suite to BrowserStack - echo "Uploading test suite to BrowserStack..." - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ - -F "file=@react-native-expo/maestro-tests.zip" \ - -F "custom_id=ReactNative-Expo-iOS-Tests-${{ github.run_id }}") - - TEST_SUITE_URL=$(echo $response | yq eval -p=json '.test_suite_url') - if [ -z "$TEST_SUITE_URL" ] || [ "$TEST_SUITE_URL" = "null" ]; then - echo "Error: Failed to upload test suite" - echo "Response: $response" - exit 1 - fi - echo "Test suite uploaded: $TEST_SUITE_URL" - - # Execute tests - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.["react-native-expo"].ios.devices' .github/browserstack-devices.yml) - echo "Loaded devices: $DEVICES" - - response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/ios/build" \ - -H "Content-Type: application/json" \ - -d "{ - \"app\": \"$APP_URL\", - \"testSuite\": \"$TEST_SUITE_URL\", - \"project\": \"${{ steps.build-info-ios.outputs.project-name }}\", - \"buildName\": \"${{ steps.build-info-ios.outputs.build-name }}\", - \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", - \"devices\": $DEVICES, - \"execute\": [\"01-app-launch-and-seeded-tasks-ios.yaml\"], - \"setEnvVariables\": { - \"MAESTRO_DITTO_CLOUD_TASK_TITLE\": \"$TASK_TITLE\" - }, - \"deviceLogs\": true, - \"networkLogs\": true, - \"video\": true - }") - - echo "Execution response: $response" - build_id=$(echo $response | yq eval '.build_id') - - if [ "$build_id" = "null" ] || [ -z "$build_id" ]; then - echo "āŒ Failed to create BrowserStack build" - echo "Response: $response" - exit 1 - fi - - echo "BrowserStack Build ID: $build_id" - echo "build_id=$build_id" >> $GITHUB_OUTPUT - - # Wait for test completion and get results - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=10 # Check every 10 seconds - ELAPSED=0 - - echo "ā³ Waiting for test execution to complete..." - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - response=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id") - - status=$(echo $response | yq eval '.status') - echo "Test status: $status (${ELAPSED}s elapsed)" - - if [ "$status" = "passed" ] || [ "$status" = "failed" ]; then - echo "Tests completed with status: $status" - echo $response | jq '.' - - if [ "$status" = "failed" ]; then - echo "āŒ iOS Maestro tests failed!" - exit 1 - else - echo "āœ… iOS Maestro tests passed!" - fi - break - elif [ "$status" = "error" ]; then - echo "āŒ BrowserStack reported build error. Fetching session details..." - echo $response | jq '.' - echo $response | yq eval '.devices[]?.sessions[]?.id' 2>/dev/null | while read sid; do - if [ -n "$sid" ]; then - echo "Session $sid details:" - curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id/sessions/$sid" | jq '.' - fi - done - exit 1 - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - if [ $ELAPSED -ge $MAX_WAIT_TIME ]; then - echo "āŒ Tests timed out after ${MAX_WAIT_TIME}s (18 minutes)" - exit 1 - fi - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [browserstack-android, browserstack-ios] - if: always() - - steps: - - name: Generate BrowserStack Summary - run: | - echo "## šŸ“± React Native Expo - BrowserStack Maestro Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Android results - echo "### Android Tests" >> $GITHUB_STEP_SUMMARY - ANDROID_BUILD_ID="${{ needs.browserstack-android.outputs.build_id }}" - ANDROID_RESULT="${{ needs.browserstack-android.result }}" - - if [ -n "$ANDROID_BUILD_ID" ] && [ "$ANDROID_BUILD_ID" != "null" ]; then - if [ "$ANDROID_RESULT" = "success" ]; then - echo "āœ… **Status:** Passed" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "šŸ”— **[View Dashboard](https://app-automate.browserstack.com/dashboard/v2/builds/$ANDROID_BUILD_ID/)**" >> $GITHUB_STEP_SUMMARY - echo "Build ID: \`$ANDROID_BUILD_ID\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed (No build created)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # iOS results - echo "### iOS Tests" >> $GITHUB_STEP_SUMMARY - IOS_BUILD_ID="${{ needs.browserstack-ios.outputs.build_id }}" - IOS_RESULT="${{ needs.browserstack-ios.result }}" - - if [ -n "$IOS_BUILD_ID" ] && [ "$IOS_BUILD_ID" != "null" ]; then - if [ "$IOS_RESULT" = "success" ]; then - echo "āœ… **Status:** Passed" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "šŸ”— **[View Dashboard](https://app-automate.browserstack.com/dashboard/v2/builds/$IOS_BUILD_ID/)**" >> $GITHUB_STEP_SUMMARY - echo "Build ID: \`$IOS_BUILD_ID\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - else - echo "āŒ **Status:** Failed (No build created)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # Summary - echo "---" >> $GITHUB_STEP_SUMMARY - if [ "$ANDROID_RESULT" = "success" ] && [ "$IOS_RESULT" = "success" ]; then - echo "āœ… **Overall Result:** All tests passed" >> $GITHUB_STEP_SUMMARY - else - echo "āš ļø **Overall Result:** Some tests failed" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/rust-tui-ci.yml b/.github/workflows/rust-tui-ci.yml deleted file mode 100644 index b17e7f42d..000000000 --- a/.github/workflows/rust-tui-ci.yml +++ /dev/null @@ -1,165 +0,0 @@ -name: Rust TUI CI -on: - pull_request: - paths: - - 'rust-tui/**' - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.91.1 - override: true - components: rustfmt, clippy - - - name: Run clippy - working-directory: rust-tui - run: cargo clippy -- -D warnings - - - name: Check formatting - working-directory: rust-tui - run: | - cargo fmt --check - if [ $? -ne 0 ]; then - echo "āŒ Code formatting issues found. Run 'cargo fmt' to fix." - exit 1 - fi - - build: - name: Build - strategy: - matrix: - os: ["macos-latest", "ubuntu-latest"] - runs-on: ${{ matrix.os }} - needs: lint - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.91.1 - override: true - components: rustfmt, clippy - - - name: Build project - working-directory: rust-tui - run: | - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - export RUSTFLAGS="-C prefer-dynamic" - fi - cargo build - - test: - name: Test - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.91.1 - override: true - components: rustfmt, clippy - - - name: Run tests - working-directory: rust-tui - run: cargo test - - integration-test: - name: Integration Test - # Disabled: Ditto Cloud API key expired/invalid since 2026-03-16. See DEVX-759. - if: false - runs-on: ubuntu-latest - needs: [lint, build, test] - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.91.1 - override: true - components: rustfmt, clippy - - - name: Create .env file - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Seed test document to Ditto Cloud - id: seed - uses: ./.github/actions/seed-ditto-document - with: - ditto-api-key: ${{ secrets.DITTO_API_KEY }} - ditto-api-url: ${{ secrets.DITTO_API_URL }} - app-name: 'rust-tui' - - - name: Build and run integration test - working-directory: rust-tui - env: - DITTO_CLOUD_TASK_TITLE: ${{ steps.seed.outputs.document-title }} - GITHUB_RUN_ID: ${{ github.run_id }} - GITHUB_RUN_NUMBER: ${{ github.run_number }} - run: | - # Build integration test - cargo build --bin integration_test - # Run integration test - cargo run --bin integration_test - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build, test, integration-test] - if: always() - - steps: - - name: Report Results - run: | - echo "## šŸ¦€ Rust TUI CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build.result }}" == "success" && \ - "${{ needs.test.result }}" == "success" && \ - "${{ needs.integration-test.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Test | ${{ needs.test.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Integration Test | ${{ needs.integration-test.result == 'success' && 'āœ… Passed' || (needs.integration-test.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/security-update-claude.yml b/.github/workflows/security-update-claude.yml deleted file mode 100644 index f9340b672..000000000 --- a/.github/workflows/security-update-claude.yml +++ /dev/null @@ -1,51 +0,0 @@ -# Per-repo caller workflow for automated security patching. -# The reusable workflow lives in getditto/sec-tools-public. - -name: 'Security Dependency Update (Claude Code)' - -on: - workflow_dispatch: - inputs: - ecosystem: - description: "Package ecosystem (npm, cargo, docker, go, python)" - required: true - type: string - alerts: - description: "JSON array of {package, version, cve, manifest}" - required: true - type: string - linear_tickets: - description: "Comma-separated Linear ticket IDs" - required: false - type: string - batch_id: - description: "Unique batch identifier" - required: true - type: string - reviewers: - description: "Comma-separated GitHub teams/users to request review from" - required: false - type: string - default: "security-team" - target_branch: - description: 'Target branch for checkout and PR base (default: dispatch ref)' - required: false - type: string - -permissions: - contents: write - pull-requests: write - -jobs: - forward-to-sec-tools-public: - uses: getditto/sec-tools-public/.github/workflows/security-update-claude.yml@main - with: - ecosystem: ${{ inputs.ecosystem }} - alerts: ${{ inputs.alerts }} - linear_tickets: ${{ inputs.linear_tickets }} - batch_id: ${{ inputs.batch_id }} - reviewers: ${{ inputs.reviewers }} - target_branch: ${{ inputs.target_branch }} - secrets: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - GH_TOKEN_SECURITY_PATCHES: ${{ secrets.GH_TOKEN_SECURITY_PATCHES }} diff --git a/.github/workflows/swift-ci.yml b/.github/workflows/swift-ci.yml deleted file mode 100644 index 640156061..000000000 --- a/.github/workflows/swift-ci.yml +++ /dev/null @@ -1,563 +0,0 @@ -name: Swift CI - -on: - pull_request: - paths: - - "swift/**" - - ".github/workflows/swift-ci.yml" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint - runs-on: macos-latest - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "latest-stable" - - - name: Create .env file (test credentials for linting) - run: | - echo "DITTO_APP_ID=test_app_id" > .env - echo "DITTO_PLAYGROUND_TOKEN=test_token" >> .env - echo "DITTO_AUTH_URL=https://test.com" >> .env - echo "DITTO_WEBSOCKET_URL=wss://test.com" >> .env - - - name: Generate Env.swift (test) - working-directory: swift - run: | - chmod +x buildEnv.sh - ./buildEnv.sh ../.env Tasks/ - - - name: Install SwiftLint - run: | - if ! command -v swiftlint &> /dev/null; then - echo "Installing SwiftLint..." - brew install swiftlint - fi - swiftlint version - - - name: Run SwiftLint - working-directory: swift - run: | - echo "šŸ” Running SwiftLint analysis..." - swiftlint lint --config .swiftlint.yml - - build-macos: - name: Build macOS - runs-on: macos-latest - needs: lint - timeout-minutes: 20 - - steps: - - uses: actions/checkout@v4 - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "latest-stable" - - - name: Create .env file (production credentials) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Generate Env.swift (production) - working-directory: swift - run: | - chmod +x buildEnv.sh - ./buildEnv.sh ../.env Tasks/ - - - name: Resolve Package Dependencies - working-directory: swift - run: | - echo "šŸ“¦ Resolving Swift Package dependencies..." - xcodebuild -resolvePackageDependencies \ - -project Tasks.xcodeproj \ - -scheme Tasks - - - name: Build macOS App - working-directory: swift - run: | - echo "šŸŽ Building macOS app..." - xcodebuild build \ - -project Tasks.xcodeproj \ - -scheme Tasks \ - -configuration Debug \ - -destination 'platform=macOS' \ - -allowProvisioningUpdates \ - SWIFT_TREAT_WARNINGS_AS_ERRORS=NO \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO - - build-ios: - name: Build iOS - runs-on: macos-latest - needs: lint - timeout-minutes: 20 - outputs: - ios-build-success: ${{ steps.build-status.outputs.success }} - - steps: - - uses: actions/checkout@v4 - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "latest-stable" - - - name: Create .env file (production credentials) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Generate Env.swift (production) - working-directory: swift - run: | - chmod +x buildEnv.sh - ./buildEnv.sh ../.env Tasks/ - - - name: Resolve Package Dependencies - working-directory: swift - run: | - echo "šŸ“¦ Resolving Swift Package dependencies..." - xcodebuild -resolvePackageDependencies \ - -project Tasks.xcodeproj \ - -scheme Tasks - - - name: Build iOS Simulator - working-directory: swift - run: | - echo "šŸ”Ø Building iOS app for simulator..." - xcodebuild build \ - -project Tasks.xcodeproj \ - -scheme Tasks \ - -configuration Debug \ - -sdk iphonesimulator \ - -destination 'generic/platform=iOS Simulator' \ - -allowProvisioningUpdates \ - SWIFT_TREAT_WARNINGS_AS_ERRORS=NO \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO \ - ONLY_ACTIVE_ARCH=NO - - - name: Build iOS Device Archive and IPA (for BrowserStack) - working-directory: swift - run: | - echo "šŸŽ Building iOS device .ipa for BrowserStack..." - - # Build and archive iOS app for real device - xcodebuild -project Tasks.xcodeproj \ - -scheme Tasks \ - -configuration Debug \ - -destination 'generic/platform=iOS' \ - -archivePath build/Tasks.xcarchive \ - archive \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO - - echo "šŸ“¦ Creating unsigned .ipa for BrowserStack..." - - # Find the .app bundle from the archive - APP_BUNDLE_PATH=$(find build/Tasks.xcarchive/Products/Applications -maxdepth 1 -name "*.app" -type d | head -1) - - if [ -d "$APP_BUNDLE_PATH" ]; then - echo "āœ… iOS app bundle found: $APP_BUNDLE_PATH" - - # Create unsigned IPA: Payload/.app zipped as .ipa - mkdir -p build/Payload - cp -R "$APP_BUNDLE_PATH" build/Payload/ - (cd build && zip -qry Tasks-unsigned.ipa Payload && rm -rf Payload) - - if [ -f "build/Tasks-unsigned.ipa" ]; then - echo "āœ… Unsigned .ipa created successfully" - ls -la build/Tasks-unsigned.ipa - else - echo "āŒ Failed to create .ipa file" - exit 1 - fi - else - echo "āŒ iOS app bundle not found in archive" - exit 1 - fi - - - name: Upload iOS IPA Artifact - uses: actions/upload-artifact@v4 - with: - name: ios-ipa-${{ github.run_id }} - path: swift/build/Tasks-unsigned.ipa - retention-days: 1 - - - name: Set build status - id: build-status - run: | - echo "success=true" >> $GITHUB_OUTPUT - - browserstack: - name: BrowserStack iOS Testing - runs-on: macos-latest - needs: [build-ios] - if: needs.build-ios.outputs.ios-build-success == 'true' - timeout-minutes: 150 - outputs: - build_id: ${{ steps.test.outputs.build_id }} - - steps: - - uses: actions/checkout@v4 - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "latest-stable" - - - name: Create .env file (production credentials for BrowserStack API) - run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - - - name: Generate Env.swift (production for XCUITest build) - working-directory: swift - run: | - chmod +x buildEnv.sh - ./buildEnv.sh ../.env Tasks/ - - - name: Download iOS IPA Artifact - uses: actions/download-artifact@v4 - with: - name: ios-ipa-${{ github.run_id }} - path: ./ - - - name: Build XCUITest Bundle for BrowserStack - id: build_test_bundle - working-directory: swift - run: | - set -euo pipefail - - echo "🧪 build-for-testing (device) with GitHub run info injected..." - DERIVED="$PWD/build/DerivedData" - PRODUCTS_DIR="$DERIVED/Build/Products" - - # Inject GitHub run info as build settings for the test - xcodebuild build-for-testing \ - -project Tasks.xcodeproj \ - -scheme Tasks \ - -configuration Debug \ - -destination 'generic/platform=iOS' \ - -derivedDataPath "$DERIVED" \ - CODE_SIGNING_ALLOWED=NO \ - CODE_SIGNING_REQUIRED=NO \ - GITHUB_RUN_ID=${{ github.run_id }} \ - GITHUB_RUN_NUMBER=${{ github.run_number }} - - echo "šŸ” Looking for Runner.app (device) and .xctestrun…" - # Runner.app must come from Debug-iphoneos (device), not simulator - RUNNER_APP=$(find "$PRODUCTS_DIR" -type d -path "*/Debug-iphoneos/*-Runner.app" -print -quit || true) - XCTESTRUN=$(find "$PRODUCTS_DIR" -maxdepth 1 -type f -name "*.xctestrun" -print -quit || true) - - if [[ -z "${RUNNER_APP:-}" || ! -d "$RUNNER_APP" ]]; then - echo "āŒ Runner.app not found under $PRODUCTS_DIR" - find "$PRODUCTS_DIR" -type d -name "*-Runner.app" || true - exit 1 - fi - - if [[ -z "${XCTESTRUN:-}" || ! -f "$XCTESTRUN" ]]; then - echo "āŒ .xctestrun not found at $PRODUCTS_DIR root" - ls -la "$PRODUCTS_DIR" || true - exit 1 - fi - - echo "āœ… Runner: $RUNNER_APP" - echo "āœ… xctestrun: $XCTESTRUN" - - # Create ZIP with BOTH files at ZIP ROOT (exactly as BrowserStack expects) - OUT_DIR="$PWD/../build" - mkdir -p "$OUT_DIR" - OUT_ZIP="$OUT_DIR/TasksUITests.zip" - rm -f "$OUT_ZIP" - - echo "šŸ“¦ Creating $OUT_ZIP with root: [$(basename "$RUNNER_APP")]" - echo "ā„¹ļø BrowserStack error requested removing .xctestrun file - trying Runner.app only" - ( cd "$(dirname "$RUNNER_APP")" && zip -qry "$OUT_ZIP" "$(basename "$RUNNER_APP")" ) - - echo "šŸ” ZIP contents:" - unzip -l "$OUT_ZIP" | sed -n '1,120p' - - echo "test_bundle_path=$OUT_ZIP" >> "$GITHUB_OUTPUT" - echo "āœ… XCUITest test-suite zip ready: $OUT_ZIP" - - - name: Upload App and Test Bundle to BrowserStack - id: upload - run: | - echo "šŸ“¤ Uploading iOS app and XCUITest bundle to BrowserStack..." - - IPA_FILE="$(pwd)/Tasks-unsigned.ipa" - TEST_BUNDLE="${{ steps.build_test_bundle.outputs.test_bundle_path }}" - - echo "šŸ“± App file: $IPA_FILE" - echo "🧪 Test bundle: $TEST_BUNDLE" - - # Validate both files exist - if [ ! -f "$IPA_FILE" ]; then - echo "āŒ IPA file not found: $IPA_FILE" - exit 1 - fi - - if [ ! -f "$TEST_BUNDLE" ]; then - echo "āŒ Test bundle not found: $TEST_BUNDLE" - exit 1 - fi - - # Upload app to BrowserStack XCUITest v2 API - echo "šŸ“± Uploading app..." - APP_UPLOAD_RESPONSE=$(curl --fail --silent --show-error -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/app" \ - -F "file=@$IPA_FILE" \ - -F "custom_id=ditto-swift-app") - - echo "App upload response: $APP_UPLOAD_RESPONSE" - APP_URL=$(echo "$APP_UPLOAD_RESPONSE" | yq eval -p=json .app_url) - - if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then - echo "āŒ Failed to upload app" - echo "Response: $APP_UPLOAD_RESPONSE" - exit 1 - fi - - # Upload test bundle to XCUITest v2 API - echo "🧪 Uploading test bundle..." - echo "Debug: Test bundle path: $TEST_BUNDLE" - ls -la "$TEST_BUNDLE" || echo "Test bundle file not found!" - - # Upload with better error handling to see 422 details - TEST_UPLOAD_RESPONSE=$(curl -s -w "\n%{http_code}" -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/test-suite" \ - -F "file=@$TEST_BUNDLE") - - HTTP_CODE=$(echo "$TEST_UPLOAD_RESPONSE" | tail -n1) - BODY=$(echo "$TEST_UPLOAD_RESPONSE" | sed '$d') - - echo "Test upload HTTP $HTTP_CODE" - echo "Response body: $BODY" - - if [ "$HTTP_CODE" -ne 200 ] && [ "$HTTP_CODE" -ne 201 ]; then - echo "āŒ Upload failed"; exit 1 - fi - - # Prefer test_suite_url, fallback to test_url - TEST_URL=$(echo "$BODY" | yq eval -p=json '.test_suite_url // .test_url // ""') - - if [ -z "$TEST_URL" ]; then - echo "āŒ Failed to upload test bundle - no test suite URL in response" - echo "Response: $BODY" - exit 1 - fi - - echo "app_url=$APP_URL" >> $GITHUB_OUTPUT - echo "test_url=$TEST_URL" >> $GITHUB_OUTPUT - echo "āœ… App and test bundle uploaded successfully" - echo "šŸ“± App URL: $APP_URL" - echo "🧪 Test URL: $TEST_URL" - - - name: Get BrowserStack build info - id: build-info - uses: ./.github/actions/generate-browserstack-names - - - name: Seed and execute XCUITests on BrowserStack - id: test - uses: nick-fields/retry@v3 - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_swift_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_swift_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_swift_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Validate inputs - APP_URL="${{ steps.upload.outputs.app_url }}" - TEST_URL="${{ steps.upload.outputs.test_url }}" - - echo "App URL: $APP_URL" - echo "Test URL: $TEST_URL" - - if [ -z "$APP_URL" ] || [ "$APP_URL" = "null" ]; then - echo "Error: No valid app URL available" - exit 1 - fi - - if [ -z "$TEST_URL" ] || [ "$TEST_URL" = "null" ]; then - echo "āš ļø No test bundle available - skipping automated testing" - echo "šŸ“± App is available for manual testing in BrowserStack dashboard" - exit 0 - fi - - # Execute tests - # Load devices from centralized config - DEVICES=$(yq eval -o=json -I=0 '.swift.devices' .github/browserstack-devices.yml) - - BUILD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/build" \ - -H "Content-Type: application/json" \ - -d "{ - \"app\": \"$APP_URL\", - \"testSuite\": \"$TEST_URL\", - \"devices\": $DEVICES, - \"project\": \"${{ steps.build-info.outputs.project-name }}\", - \"buildName\": \"${{ steps.build-info.outputs.build-name }}\", - \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", - \"deviceLogs\": true, - \"video\": true, - \"networkLogs\": true, - \"setEnvVariables\": { - \"GITHUB_RUN_ID\": \"${{ github.run_id }}\", - \"GITHUB_RUN_NUMBER\": \"${{ github.run_number }}\", - \"DITTO_CLOUD_TASK_TITLE\": \"$TASK_TITLE\" - } - }") - - echo "BrowserStack API Response:" - echo "$BUILD_RESPONSE" - - BUILD_ID=$(echo "$BUILD_RESPONSE" | yq eval .build_id) - - if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then - echo "Error: Failed to create BrowserStack build" - echo "Response: $BUILD_RESPONSE" - exit 1 - fi - - echo "Build started with ID: $BUILD_ID" - echo "build_id=$BUILD_ID" >> "$GITHUB_OUTPUT" - - # Wait for XCUITest to complete - MAX_WAIT_TIME=1080 # 18 minutes - CHECK_INTERVAL=30 # Check every 30 seconds - ELAPSED=0 - - while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do - BUILD_STATUS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/builds/$BUILD_ID") - - BUILD_STATUS=$(echo "$BUILD_STATUS_RESPONSE" | yq eval .status) - - if [ "$BUILD_STATUS" = "null" ] || [ -z "$BUILD_STATUS" ]; then - echo "Error getting build status. Response: $BUILD_STATUS_RESPONSE" - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - continue - fi - - echo "Build status: $BUILD_STATUS (elapsed: ${ELAPSED}s)" - - if [ "$BUILD_STATUS" = "done" ] || [ "$BUILD_STATUS" = "failed" ] || [ "$BUILD_STATUS" = "error" ] || [ "$BUILD_STATUS" = "passed" ] || [ "$BUILD_STATUS" = "completed" ]; then - echo "Build completed with status: $BUILD_STATUS" - break - fi - - sleep $CHECK_INTERVAL - ELAPSED=$((ELAPSED + CHECK_INTERVAL)) - done - - # Get final results - FINAL_RESULT=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/builds/$BUILD_ID") - - echo "Final build result:" - echo "$FINAL_RESULT" | jq . - - # Check final status - BUILD_STATUS=$(echo "$FINAL_RESULT" | yq eval .status) - if [ "$BUILD_STATUS" != "passed" ]; then - echo "Build failed with status: $BUILD_STATUS" - exit 1 - else - echo "All tests passed successfully!" - fi - - summary: - name: CI Report - runs-on: ubuntu-latest - needs: [lint, build-macos, build-ios, browserstack] - if: always() - - steps: - - name: Report Results - run: | - echo "## šŸ“± Swift CI" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Overall status - if [[ "${{ needs.lint.result }}" == "success" && \ - "${{ needs.build-macos.result }}" == "success" && \ - "${{ needs.build-ios.result }}" == "success" && \ - "${{ needs.browserstack.result }}" == "success" ]]; then - echo "**Overall Status:** āœ… All checks passed" >> $GITHUB_STEP_SUMMARY - else - echo "**Overall Status:** āŒ Failed" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build macOS | ${{ needs.build-macos.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build iOS | ${{ needs.build-ios.result == 'success' && 'āœ… Passed' || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| BrowserStack Tests | ${{ needs.browserstack.result == 'success' && 'āœ… Passed' || (needs.browserstack.result == 'skipped' && 'ā­ļø Skipped') || 'āŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # BrowserStack link - if [[ "${{ needs.browserstack.result }}" != "skipped" ]]; then - echo "### BrowserStack Session" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "šŸŽ [View Test Results](https://app-automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Tested Device:**" >> $GITHUB_STEP_SUMMARY - echo "- iPhone 15 Pro (iOS 17)" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/test-build-info-action.yml b/.github/workflows/test-build-info-action.yml deleted file mode 100644 index 0c557d8dd..000000000 --- a/.github/workflows/test-build-info-action.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Test Build Info Action - -on: - workflow_dispatch: - -jobs: - test-build-info: - name: Test Build Info Generation - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Test generate names action - id: build-info - uses: ./.github/actions/generate-browserstack-names - with: - platform-suffix: ' (Test Platform)' - - - name: Display outputs - run: | - echo "## 🧪 Build Info Action Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Outputs:" >> $GITHUB_STEP_SUMMARY - echo "- **Project Name:** \`${{ steps.build-info.outputs.project-name }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Build Name:** \`${{ steps.build-info.outputs.build-name }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **PR Number:** \`${{ steps.build-info.outputs.pr-number }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **PR Title:** \`${{ steps.build-info.outputs.pr-title }}\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Context Info:" >> $GITHUB_STEP_SUMMARY - echo "- **Event Name:** \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Head Ref:** \`${{ github.head_ref }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Ref Name:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Run Number:** \`${{ github.run_number }}\`" >> $GITHUB_STEP_SUMMARY - - echo "" - echo "āœ… If you see 'PR-XXX: ' in Build Name above, the action is working!" - echo "āŒ If you see 'Build-XXX', the PR fetch failed (expected for workflow_dispatch on branch)" diff --git a/.github/workflows/update-sdk-versions.yml b/.github/workflows/update-sdk-versions.yml deleted file mode 100644 index fc6a89429..000000000 --- a/.github/workflows/update-sdk-versions.yml +++ /dev/null @@ -1,129 +0,0 @@ -name: Update Ditto SDK Versions - -on: - workflow_dispatch: - inputs: - version: - description: "SDK version to update to" - required: true - type: string - default: "4.12.0" - -jobs: - update-sdk-versions: - # NOTE: We use macos-latest instead of ubuntu-latest because this workflow needs to run - # CocoaPods commands for updating React Native iOS dependencies. CocoaPods requires a - # macOS environment with Xcode. - runs-on: macos-latest - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "npm" - # Note: react-native-expo uses yarn (not npm), so it's excluded from npm cache paths - cache-dependency-path: | - react-native/package-lock.json - javascript-tui/package-lock.json - javascript-web/package-lock.json - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "9.0.x" - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Flutter - uses: subosito/flutter-action@v2.16.0 - with: - channel: "stable" - cache: true - - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - - - name: Setup Ruby and Bundler - uses: ruby/setup-ruby@v1.267.0 - with: - ruby-version: "3.3" - # working-directory specifies where to find Gemfile for bundler-cache - bundler-cache: true - working-directory: react-native - - - name: Setup CocoaPods - run: | - sudo gem install cocoapods - pod --version - - - name: Update SDK versions and lockfiles - # The Android Kotlin build requires all four Ditto environment variables to be set - # for any Gradle operation (including dependency refresh). These are validated at - # configuration time in android-kotlin/QuickStartTasks/app/build.gradle.kts - env: - DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} - DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} - DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} - DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} - run: node scripts/bump-versions.js -t "${{ github.event.inputs.version }}" - - - name: Configure Git - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - - name: Create branch and commit changes - id: create-branch - run: | - BRANCH_NAME="sdk-update-${{ github.event.inputs.version }}-$(date +%Y%m%d-%H%M%S)" - echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - git checkout -b "$BRANCH_NAME" - git add -A - - if git diff --staged --quiet; then - echo "No changes to commit" - echo "has-changes=false" >> $GITHUB_OUTPUT - else - git commit -m "chore: update Ditto SDK to version ${{ github.event.inputs.version }}" - - git push origin "$BRANCH_NAME" - echo "has-changes=true" >> $GITHUB_OUTPUT - fi - - - name: Create Draft Pull Request - if: steps.create-branch.outputs.has-changes == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr create \ - --title "chore: update Ditto SDK to version ${{ github.event.inputs.version }}" \ - --body "$(cat <<'EOF' - Updates all quickstart applications to use Ditto SDK version ${{ github.event.inputs.version }} - - - Updates lockfiles where possible - - Excludes Kotlin Multiplatform and Java Spring - EOF - )" \ - --draft \ - --head "${{ steps.create-branch.outputs.branch-name }}" \ - --base main - - - name: Output PR URL - if: steps.create-branch.outputs.has-changes == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_URL=$(gh pr view "${{ steps.create-branch.outputs.branch-name }}" --json url -q .url) - echo "Created draft PR: $PR_URL" diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index c2bf476d4..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,269 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Repository Overview - -This is a multi-platform quickstart repository for the Ditto SDK, containing sample "Tasks" applications that demonstrate real-time synchronization across various programming languages and platforms. Each subdirectory contains a complete, self-contained application implementing a todo list manager with Ditto's peer-to-peer sync capabilities. - -## Initial Setup - -Before working with any app: -1. Ensure `.env` exists (copy from `.env.sample`) -2. Configure Ditto credentials: - - `DITTO_APP_ID` - - `DITTO_PLAYGROUND_TOKEN` - - `DITTO_AUTH_URL` - - `DITTO_WEBSOCKET_URL` - -## Common Development Commands - -### Android/Java/Kotlin Projects -```bash -# Build project -./gradlew build - -# Run tests -./gradlew test - -# Run specific test -./gradlew test --tests "*ClassName.testMethodName" - -# Install on device/emulator -./gradlew installDebug - -# Clean build -./gradlew clean - -# List all tasks -./gradlew tasks -``` - -### JavaScript/TypeScript Projects -```bash -# Install dependencies -npm install - -# Development server -npm run dev - -# Production build -npm run build - -# Run linter -npm run lint - -# Format code -npm run format - -# Run tests (where available) -npm test - -# Run specific test -npm test -- --testNamePattern="test name" -``` - -### Rust Projects -```bash -# Build project -cargo build - -# Run application -cargo run - -# Run tests -cargo test - -# Run specific test -cargo test test_name - -# Format code -cargo fmt - -# Check code -cargo clippy -``` - -### Flutter Projects -```bash -# Get dependencies -flutter pub get - -# Run app -flutter run - -# Run tests -flutter test - -# Run specific test -flutter test test/widget_test.dart - -# Build for specific platform -flutter build apk/ios/web -``` - -### .NET Projects -```bash -# Build project -dotnet build - -# Run application -dotnet run - -# Run tests -dotnet test - -# Run specific test -dotnet test --filter "FullyQualifiedName~TestMethodName" -``` - -### C++ Projects -```bash -# Configure build -cmake . - -# Build project -make - -# Clean build -make clean -``` - -### Tools Setup -```bash -# Install Android command line tools (if ANDROID_HOME is set) -just cmdline_tools - -# Install required Android SDK components -just tools -``` - -## Architecture Overview - -### Common Structure -All applications implement the same "Tasks" functionality: -- **Task Model**: Simple todo items with ID, text/title, and completion status -- **Real-time Sync**: Automatic synchronization between devices using Ditto -- **CRUD Operations**: Create, read, update, and delete tasks -- **Platform UI**: Native UI patterns for each platform - -### Ditto SDK Integration Pattern -1. **Initialization**: Apps read credentials from environment variables -2. **Identity**: Uses "Online Playground" identity (development only) -3. **Collection**: All apps use a "tasks" collection -4. **Document Structure**: Mostly consistent across platforms (slight variations in field names): - ```json - { - "_id": "unique-id", - "text": "Task description", // or "title" in some implementations - "isCompleted": false, // or "done" in some implementations - "deleted": false // soft delete in some implementations - } - ``` - -### Platform-Specific Patterns - -#### Android/Kotlin -- MVVM architecture with ViewModels -- Jetpack Compose for UI -- Gradle build with version catalogs (`gradle/libs.versions.toml`) -- Environment variables loaded via custom `loadEnvProperties()` function -- Task model uses `title`, `done`, and `deleted` fields - -#### JavaScript Web -- React with TypeScript -- Vite build system -- Tailwind CSS for styling -- ESLint + Prettier for code quality -- Task model uses `text` and `isCompleted` fields - -#### iOS/Swift -- SwiftUI for interface -- Xcode project structure -- Swift Package Manager for dependencies -- Environment variables loaded via `buildEnv.sh` script -- Task model uses `text` and `isCompleted` fields - -#### Rust TUI -- Tokio async runtime -- Ratatui for terminal UI -- Event-driven architecture -- Task model uses `text` and `isCompleted` fields - -#### React Native -- React Native with TypeScript -- Metro bundler -- Custom components for modals and UI elements -- Task model uses `text` and `isCompleted` fields - -#### Kotlin Multiplatform -- Compose Multiplatform UI -- Targets iOS, Android, and Desktop -- Shared business logic across platforms -- Task model uses `text` and `isCompleted` fields - -## Key Development Considerations - -### Environment Variables -All apps load configuration from the `.env` file at the repository root. The loading mechanism varies by platform: -- **Android**: Custom Gradle function in `build.gradle.kts` that reads `.env` and sets build config fields -- **JavaScript**: Vite environment variables (auto-loaded from `.env`) -- **Rust**: `dotenvy` crate -- **Flutter**: `flutter_dotenv` package -- **Swift**: `buildEnv.sh` script generates `Env.swift` file -- **.NET**: Custom environment loading in code - -### Testing -- Android: JUnit tests in `src/test` and instrumented tests in `src/androidTest` -- JavaScript: Jest for React Native, Vitest for web apps -- Rust: Built-in `cargo test` -- Flutter: `flutter test` with widget and unit tests -- Swift: XCTest framework - -### Code Quality Tools -- **JavaScript/TypeScript**: ESLint + Prettier -- **Kotlin**: Built-in Kotlin linting -- **Rust**: `cargo fmt` and `cargo clippy` -- **Swift**: SwiftLint (where configured) - -## Important Notes - -1. **Production Warning**: These apps use "Online Playground" identity which is NOT suitable for production - - For production environments, configure use the "Online with Authentication" identity to authenticate with - your identity system and retrieve a secret token prior to syncing with Ditto. - - Refer to the [Authentication and Authorization](https://docs.ditto.live/key-concepts/authentication-and-authorization#online-with-authentication) - for detailed instructions on configuring identity for production use. -2. **Cross-Platform Consistency**: All apps implement the same data model for interoperability (though field names may vary slightly) -3. **Platform Best Practices**: Each app follows its platform's conventions and patterns -4. **Real Device Testing**: For full P2P functionality, test on real devices rather than simulators -5. **Data Model Variations**: While conceptually the same, implementations vary in field naming: - - Some use `text`/`isCompleted`, others use `title`/`done` - - Some include soft delete functionality with a `deleted` field - ---- - -## Security Patching - -When patching Dependabot alerts, bundle ALL fixes across ALL apps into a single PR. - -### Discovery - -Use `gh api 'repos/getditto/quickstart/dependabot/alerts?state=open&severity=critical,high&per_page=100'` to get open alerts. Group by manifest file. - -### Fix priority order (try in sequence, stop at first success) - -1. **Use the package manager's audit fix** — `npm audit fix`, `bundle audit`, `cargo audit fix`, etc. Use `--legacy-peer-deps` for npm if peer conflicts arise. -2. **Add a resolution/override** — Only when the parent's declared semver range already includes the patched version but the lockfile pins an old resolution. Use `"overrides"` (npm) or `"resolutions"` (yarn) in `package.json`. For other ecosystems, use the equivalent mechanism. -3. **Bump the parent dependency** — When the parent's range excludes the patched version. Prefer patch/minor bumps within the same major version. -4. **Flag for manual intervention** — If none of the above work, do NOT force a fix. Commit all other successful patches and add a `## MANUAL INTERVENTION REQUIRED` section to the PR body listing: the alert, what was tried, why each approach failed, and a suggested fix. - -### Verification - -After patching, verify each affected app still builds using the build commands from "Common Development Commands" above. If a build fails, revert that patch and fall back to the next approach. - -### Repo-specific notes - -- `yarn` is not globally installed. Use `npx yarn` everywhere. -- `react-native-expo/` has both `package-lock.json` and `yarn.lock` — both must be patched independently. -- `npx yarn upgrade @` adds the package as a direct dep. Use bare `npx yarn upgrade ` to avoid this. -- Native builds (Xcode, Android SDK, .NET) may not be available in CI or on your machine. For those apps, verify lockfile/manifest integrity and dependency resolution only. diff --git a/LICENSE b/LICENSE index 68d885abb..892015039 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright Ā©ļø 2024 DittoLive, Inc. +Copyright Ā©ļø 2026 DittoLive, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ā€œSoftwareā€), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index dc5ab2c41..103b8e10a 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,19 @@ documentation for more information. For support, please contact Ditto Support (). +> [!IMPORTANT] +> This repository is a read-only mirror maintained by Ditto's SDK release +> process. Pull requests opened outside that process are not accepted. To report +> a problem or request a change, contact Ditto Support (). + ## Obtaining your Ditto Identity The Ditto SDK requires you to provide an identity for your application, which may be generated using the [Ditto Portal](https://portal.ditto.live/). For the purposes of these -quickstart applications, we'll be using the "Online Playground" identity type. - -![Ditto Portal](.github/assets/ditto-portal.png) +quickstart applications, we'll be using the "Development" identity type. > [!IMPORTANT] -> The Online Playground identity type is _not_ suitable for production use. It is intended +> The Development identity type is _not_ suitable for production use. It is intended > only for development and testing purposes. To obtain your Ditto identity and configure the quickstart apps with it, follow these steps: @@ -27,7 +30,9 @@ To obtain your Ditto identity and configure the quickstart apps with it, follow 1. Copy the `.env.sample` file to `.env`. - in a terminal: `cp .env.sample .env`. - in a macOS Finder window, press `ā‡§āŒ˜.` (SHIFT+CMD+period) to show hidden files. -1. Save your App ID, Online Playground Token, Auth URL, and WebSocket URL in the `.env` file. +1. In the Ditto Portal, open your database's **Connect** tab — it lists your + **Database ID**, **Development Token**, and **Server URL**. Save these values in + the `.env` file. Please see the app-specific README files for details on the tools necessary to build and run them. @@ -36,13 +41,16 @@ build and run them. - [Android Kotlin](android-kotlin/README.md) - [Android Java](android-java/README.md) +- [Kotlin Multiplatform](kotlin-multiplatform/README.md) - [Java Server](java-server/README.md) - [C++ TUI](cpp-tui/README.md) - [C# .NET MAUI](dotnet-maui/README.md) - [C# .NET TUI](dotnet-tui/README.md) - [C# .NET Win Forms](dotnet-winforms/README.md) +- [C# .NET Win Forms (.NET Framework 4.8)](dotnet-winforms-net48/README.md) - [Electron](electron/README.md) -- [Flutter](flutter_quickstart/README.md) +- [Flutter](flutter_app/README.md) +- [Go TUI](go-tui/README.md) - [Javascript TUI](javascript-tui/README.md) - [Javascript Web](javascript-web/README.md) - [React Native](react-native/README.md) diff --git a/android-java/README.md b/android-java/README.md index ad34bf0d7..a087b4da1 100644 --- a/android-java/README.md +++ b/android-java/README.md @@ -13,7 +13,7 @@ After you have completed the [common prerequisites] you will need the following: ## Documentation - [Install Guide](https://docs.ditto.live/sdk/latest/install-guides/java/android) -- [API Reference](https://software.ditto.live/android/Ditto/5.0.0/api-reference/) +- [API Reference](https://docs.ditto.live/sdk/latest/api-reference/kotlin) - [SDK Release Notes](https://docs.ditto.live/sdk/latest/release-notes/java) [common prerequisites]: https://github.com/getditto/quickstart#common-prerequisites @@ -23,8 +23,8 @@ After you have completed the [common prerequisites] you will need the following: Assuming you have Android Studio and other prerequisites installed, you can build and run the app by following these steps: -1. Create an application at . Make note of the database ID (used to be called app ID) and online playground token. -2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID (used to be called AppId), Online Playground Token, and Auth URL. +1. Create an application at . Make note of the database ID and development token. +2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID, Development Token, and Server URL. 3. Launch Android Studio and open the `quickstart/android-java` directory. 4. In Android Studio, select a connected Android device, or create and launch an Android emulator and select it as the destination, then choose the **Run > Run 'app'** menu item. @@ -38,7 +38,14 @@ Compatible with Android Automotive OS (AAOS) The Android app is a simple to-do list app that demonstrates how to use the Ditto Android SDK to sync data with other devices. It is implemented using Java and Android Views using an Activity and a programmatically implemented RecyclerView. -The Ditto v5 SDK ships as a Kotlin module (`com.ditto:ditto-kotlin-android`) and exposes some APIs as `suspend` functions. To keep the application code idiomatic Java, this project uses a small Kotlin bridge file, `DittoHelper.kt`, that wraps the suspending APIs with `runBlocking` and exposes `@JvmStatic` entry points. All application logic — `MainActivity`, `Task`, and `TaskAdapter` — remains in Java. +The Ditto integration is split by concern, following the natural lifecycle of each piece: + +- `DittoManager.kt` — Ditto instance management: configuration, identity/auth, and starting/stopping sync. The `ditto` instance is a **process-global singleton** — created exactly once at app startup and never tied to an Activity, so a configuration change like rotation recreates the Activity without recreating Ditto (two instances on the same persistence directory would contend on its lock). It knows nothing about tasks. +- `TasksApplication.java` — the `Application` subclass that creates the Ditto singleton once at process start (via `DittoManager.initialize`) and starts sync. Registered as the app's `android:name` in the manifest. +- `TasksRepository.kt` — the tasks data concern: it registers the **app-lifetime sync subscription** (once), performs CRUD by calling the real Ditto API directly through `DittoManager.ditto`, and exposes `observeTasks(...)`, which creates a store observer and returns its handle to the caller. +- `MainActivity.java` — the UI. It **owns the store observer for the screen**: it starts observing in `onCreate` and closes the observer in `onDestroy`, so the observer is scoped to the view while the Ditto instance and the subscription live for the whole app. + +The Ditto v5 SDK ships as a Kotlin module (`com.ditto:ditto-kotlin-android`) and exposes some APIs (such as `store.execute`) as `suspend` functions. Because Java cannot call `suspend` functions directly, `DittoManager` and `TasksRepository` are written in Kotlin, which lets them invoke the real Ditto API directly (bridging the suspending calls with `runBlocking`) instead of introducing a wrapper layer over the SDK. The remaining application logic — `TasksApplication`, `MainActivity`, `Task`, and `TaskAdapter` — is in Java. It is assumed that the reader is familiar with Android development and with Java/Activity/RecyclerView, but needs some guidance on how to use Ditto. The following is a summary of the key parts of integration with Ditto. @@ -50,12 +57,3 @@ to download the Ditto SDK from Maven Central and add it to the project: ```kotlin implementation(libs.ditto) ``` - -This line in `gradle/libs.versions.toml` specifies which version of the Ditto -SDK to use: - -```kotlin -ditto = "5.0.0" -``` - -To use a newer version of the SDK, change the version number on this line. diff --git a/android-java/app/build.gradle.kts b/android-java/app/build.gradle.kts index 11840d22c..1cf6ef128 100644 --- a/android-java/app/build.gradle.kts +++ b/android-java/app/build.gradle.kts @@ -16,9 +16,9 @@ fun loadEnvProperties(): Properties { FileInputStream(envFile).use { properties.load(it) } } else { val requiredEnvVars = listOf( - "DITTO_APP_ID", - "DITTO_PLAYGROUND_TOKEN", - "DITTO_AUTH_URL" + "DITTO_DATABASE_ID", + "DITTO_DEVELOPMENT_TOKEN", + "DITTO_SERVER_URL" ) for (envVar in requiredEnvVars) { @@ -30,8 +30,8 @@ fun loadEnvProperties(): Properties { return properties } -// Define BuildConfig.DITTO_APP_ID, BuildConfig.DITTO_PLAYGROUND_TOKEN, -// and BuildConfig.DITTO_AUTH_URL based on values in the .env file +// Define BuildConfig.DITTO_DATABASE_ID, BuildConfig.DITTO_DEVELOPMENT_TOKEN, +// and BuildConfig.DITTO_SERVER_URL based on values in the .env file // // More information can be found here: // https://docs.ditto.live/sdk/latest/install-guides/java/android#integrating-and-initializing @@ -43,28 +43,28 @@ androidComponents { onVariants { val prop = loadEnvProperties() it.buildConfigFields.put( - "DITTO_APP_ID", + "DITTO_DATABASE_ID", BuildConfigField( "String", - "\"${envValue(prop, "DITTO_APP_ID")}\"", - "Ditto application ID" + "\"${envValue(prop, "DITTO_DATABASE_ID")}\"", + "Ditto database ID" ) ) it.buildConfigFields.put( - "DITTO_PLAYGROUND_TOKEN", + "DITTO_DEVELOPMENT_TOKEN", BuildConfigField( "String", - "\"${envValue(prop, "DITTO_PLAYGROUND_TOKEN")}\"", - "Ditto online playground authentication token" + "\"${envValue(prop, "DITTO_DEVELOPMENT_TOKEN")}\"", + "Ditto development authentication token" ) ) it.buildConfigFields.put( - "DITTO_AUTH_URL", + "DITTO_SERVER_URL", BuildConfigField( "String", - "\"${envValue(prop, "DITTO_AUTH_URL")}\"", - "Ditto Auth URL" + "\"${envValue(prop, "DITTO_SERVER_URL")}\"", + "Ditto Server URL" ) ) } diff --git a/android-java/app/src/main/AndroidManifest.xml b/android-java/app/src/main/AndroidManifest.xml index 48864a026..8c69ad7d4 100644 --- a/android-java/app/src/main/AndroidManifest.xml +++ b/android-java/app/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ tools:targetApi="tiramisu" /> - auth.expirationHandler = { dittoInstance, _ -> - dittoInstance.auth?.login(token, DittoAuthenticationProvider.development()) - } - } - } - - @JvmStatic - @Throws(DittoException::class) - fun execute(ditto: Ditto, query: String, args: Map) { - runBlocking { - ditto.store.execute(query, args) - } - } - - @JvmStatic - fun registerSubscription(ditto: Ditto, query: String): DittoSyncSubscription { - return ditto.sync.registerSubscription(query) - } - - @JvmStatic - fun registerObserver( - ditto: Ditto, - query: String, - callback: Consumer - ): DittoStoreObserver { - return ditto.store.registerObserver(query) { result -> - callback.accept(result) - } - } - - @JvmStatic - fun startSync(ditto: Ditto) { - ditto.sync.start() - } - - @JvmStatic - fun stopSync(ditto: Ditto) { - ditto.sync.stop() - } - - @JvmStatic - fun isSyncActive(ditto: Ditto): Boolean { - return ditto.sync.isActive - } -} diff --git a/android-java/app/src/main/java/com/example/dittotasks/DittoManager.kt b/android-java/app/src/main/java/com/example/dittotasks/DittoManager.kt new file mode 100644 index 000000000..c56ea8e23 --- /dev/null +++ b/android-java/app/src/main/java/com/example/dittotasks/DittoManager.kt @@ -0,0 +1,75 @@ +package com.example.dittotasks + +import com.ditto.kotlin.Ditto +import com.ditto.kotlin.DittoAuthenticationProvider +import com.ditto.kotlin.DittoConfig +import com.ditto.kotlin.DittoFactory + +/** + * Owns configuration and lifecycle of the Ditto instance: creating it, wiring up + * development-mode authentication, and controlling sync. It knows nothing about tasks. + * + * The Ditto instance is a process-global singleton, created exactly once at app + * startup (see [TasksApplication]) and living for the whole process. It is deliberately + * NOT tied to any Activity: a configuration change such as rotation recreates the + * Activity, but must never recreate Ditto — two live instances on the same persistence + * directory would contend on its lock. Collaborators use the real Ditto API directly + * through [ditto]. + */ +class DittoManager { + companion object { + /** The configured Ditto instance. Collaborators use the real Ditto API through this. */ + lateinit var ditto: Ditto + private set + + @JvmStatic + val isInitialized: Boolean + get() = ::ditto.isInitialized + + /** + * Create and configure the Ditto instance and set up authentication. Called once, + * from [TasksApplication.onCreate]. Does not start sync — call [startSync] for that. + */ + @JvmStatic + fun initialize(databaseId: String, serverUrl: String, authToken: String) { + if (::ditto.isInitialized) { + throw IllegalStateException("Ditto is already initialized") + } + + // Create Ditto with a server connection. + // https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing + val config = DittoConfig( + databaseId = databaseId, + connect = DittoConfig.Connect.Server(serverUrl) + ) + ditto = DittoFactory.create(config) + + // Set up the authentication handler (must be configured before sync.start()). + ditto.auth?.let { auth -> + auth.expirationHandler = { dittoInstance, _ -> + dittoInstance.auth?.login(authToken, DittoAuthenticationProvider.development()) + } + } + } + + // Start/stop sync. + // https://docs.ditto.live/sdk/latest/sync/start-and-stop-sync + @JvmStatic + fun startSync() { + if (!ditto.sync.isActive) { + ditto.sync.start() + } + } + + @JvmStatic + fun stopSync() { + if (ditto.sync.isActive) { + ditto.sync.stop() + } + } + + @JvmStatic + val isSyncActive: Boolean + get() = ditto.sync.isActive + } +} diff --git a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java index 5df56efc9..e8be96b03 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java +++ b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java @@ -18,139 +18,102 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.ditto.kotlin.Ditto; import com.ditto.kotlin.DittoStoreObserver; -import com.ditto.kotlin.DittoSyncSubscription; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; public class MainActivity extends ComponentActivity { private TaskAdapter taskAdapter; private SwitchCompat syncSwitch; - Ditto ditto; - DittoSyncSubscription taskSubscription; - DittoStoreObserver taskObserver; + // The store observer is scoped to this screen: registered in onCreate, closed in + // onDestroy. The Ditto instance and the sync subscription are app-scoped singletons + // (see DittoManager / TasksRepository) and are not owned here. + private DittoStoreObserver tasksObserver; - private final String dittoAppId = BuildConfig.DITTO_APP_ID; - private final String dittoPlaygroundToken = BuildConfig.DITTO_PLAYGROUND_TOKEN; - private final String dittoAuthUrl = BuildConfig.DITTO_AUTH_URL; + private final String dittoDatabaseId = BuildConfig.DITTO_DATABASE_ID; + private final String dittoDevelopmentToken = BuildConfig.DITTO_DEVELOPMENT_TOKEN; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - + // Keep screen on during testing to prevent NoActivityResumedException if(BuildConfig.DEBUG && isInstrumentationTest()){ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } - - initDitto(); // Populate connection info (only in debug builds) if(BuildConfig.DEBUG) { - TextView appId = findViewById(R.id.ditto_app_id); - appId.setText(String.format("App ID: %s", dittoAppId)); + TextView databaseId = findViewById(R.id.ditto_app_id); + databaseId.setText(String.format("Database ID: %s", dittoDatabaseId)); - TextView playgroundToken = findViewById(R.id.ditto_playground_token); - playgroundToken.setText(String.format("Playground Token: %s", dittoPlaygroundToken)); + TextView developmentToken = findViewById(R.id.ditto_development_token); + developmentToken.setText(String.format("Development Token: %s", dittoDevelopmentToken)); } else { // Hide credential views in production findViewById(R.id.ditto_app_id).setVisibility(View.GONE); - findViewById(R.id.ditto_playground_token).setVisibility(View.GONE); + findViewById(R.id.ditto_development_token).setVisibility(View.GONE); } // Initialize "add task" fab FloatingActionButton addButton = findViewById(R.id.add_button); addButton.setOnClickListener(v -> showAddTaskModal()); - // Initialize sync switch + // Initialize sync switch from the real, app-scoped sync state (which survives + // rotation). Set it before attaching the listener so this doesn't fire toggleSync(). syncSwitch = findViewById(R.id.sync_switch); - syncSwitch.setChecked(true); - syncSwitch.setOnCheckedChangeListener(((buttonView, isChecked) -> { - toggleSync(); - })); + boolean syncActive = DittoManager.isSyncActive(); + syncSwitch.setChecked(syncActive); + syncSwitch.setText(syncActive ? "Sync Active" : "Sync Inactive"); + syncSwitch.setTrackTintList(syncActive ? ColorStateList.valueOf(0xFFBB86FC) : null); + syncSwitch.setOnCheckedChangeListener(((buttonView, isChecked) -> toggleSync())); // Initialize task list RecyclerView taskList = findViewById(R.id.task_list); taskList.setLayoutManager(new LinearLayoutManager(this)); taskAdapter = new TaskAdapter(); taskList.setAdapter(taskAdapter); - taskAdapter.setOnTaskToggleListener((task, isChecked) -> { - toggleTask(task); - }); + taskAdapter.setOnTaskToggleListener((task, isChecked) -> toggleTask(task)); taskAdapter.setOnTaskDeleteListener(this::deleteTask); taskAdapter.setOnTaskLongPressListener(this::showEditTaskModal); - - // Initialize empty list - Ditto observer will populate it - taskAdapter.setTasks(Collections.emptyList()); - } + // Initialize empty list - the Ditto observer will populate it + taskAdapter.setTasks(Collections.emptyList()); - void initDitto() { - Log.d("DittoInit", "=== Starting Ditto initialization ==="); - - Log.d("DittoInit", "DITTO_APP_ID: " + dittoAppId); - Log.d("DittoInit", "DITTO_PLAYGROUND_TOKEN: " + (dittoPlaygroundToken != null ? "Present" : "NULL")); - Log.d("DittoInit", "DITTO_AUTH_URL: " + dittoAuthUrl); + // Wire up the tasks concern for this screen (subscription + observer). + setUpTasks(); + } + // Ditto itself is created and started once by TasksApplication; here we just wire up + // the tasks concern for this screen: register the app-wide subscription (idempotent) + // and start a store observer scoped to this Activity (closed in onDestroy). + void setUpTasks() { // Skip permission requests during testing to avoid permission dialogs if (!isInstrumentationTest()) { - Log.d("DittoInit", "Requesting permissions..."); requestPermissions(); - } else { - Log.d("DittoInit", "Skipping permissions during instrumentation test"); } - Log.d("DittoInit", "Starting Ditto SDK initialization..."); - try { - // Create Ditto with server connection - // https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing - Log.d("DittoInit", "Creating Ditto instance..."); - ditto = DittoHelper.createDitto(dittoAppId, dittoAuthUrl); - Log.d("DittoInit", "Ditto instance created successfully"); - - // Set up authentication handler (must be set before sync.start()) - Log.d("DittoInit", "Setting up authentication..."); - DittoHelper.setupAuth(ditto, dittoPlaygroundToken); - Log.d("DittoInit", "Authentication configured"); - - // register subscription - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - Log.d("DittoInit", "Registering subscription..."); - taskSubscription = DittoHelper.registerSubscription(ditto, "SELECT * FROM tasks"); - Log.d("DittoInit", "Subscription registered"); - - // register observer for live query - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - Log.d("DittoInit", "Registering observer..."); - taskObserver = DittoHelper.registerObserver(ditto, - "SELECT * FROM tasks WHERE deleted=false ORDER BY title ASC", - result -> { - Log.d("DittoInit", "Observer callback triggered with " + result.getItems().size() + " items"); - var tasks = result.getItems().stream().map(Task::fromQueryItem).collect(Collectors.toCollection(ArrayList::new)); - runOnUiThread(() -> { - Log.d("DittoInit", "Updating UI with " + tasks.size() + " tasks"); - taskAdapter.setTasks(new ArrayList<>(tasks)); - }); - }); - Log.d("DittoInit", "Observer registered"); - - Log.d("DittoInit", "Starting Ditto sync..."); - DittoHelper.startSync(ditto); - Log.d("DittoInit", "=== Ditto initialization completed successfully ==="); - } catch (Exception e) { - Log.e("DittoInit", "Error during Ditto initialization: " + e.getMessage(), e); - e.printStackTrace(); - } + TasksRepository.registerSubscription(); + tasksObserver = TasksRepository.observeTasks(tasks -> + runOnUiThread(() -> taskAdapter.setTasks(new ArrayList<>(tasks)))); + } + + @Override + protected void onDestroy() { + // The store observer is tied to this screen's lifecycle — close it so it doesn't + // leak across Activity recreation (e.g. rotation). The Ditto instance and the sync + // subscription are app-scoped singletons and intentionally live on. + if (tasksObserver != null) { + tasksObserver.close(); + tasksObserver = null; + } + super.onDestroy(); } // Check if running under instrumentation (testing) @@ -193,88 +156,23 @@ void requestPermissions() { } private void createTask(String title) { - if (ditto == null) { - Log.i("MainActivity", "Ditto disabled - create task ignored: " + title); - return; - } - - HashMap task = new HashMap<>(); - task.put("title", title); - task.put("done", false); - task.put("deleted", false); - - Map args = Collections.singletonMap("task", task); - - try { - // Add tasks into the ditto collection using DQL INSERT statement - // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents - DittoHelper.execute(ditto, "INSERT INTO tasks DOCUMENTS (:task)", args); - } catch (Exception e) { - e.printStackTrace(); - } + TasksRepository.createTask(title); } private void editTaskTitle(Task task, String newTitle) { - if (ditto == null) { - Log.i("MainActivity", "Ditto disabled - edit task ignored: " + task.getTitle()); - return; - } - - Map args = new HashMap<>(); - args.put("id", task.getId()); - args.put("title", newTitle); - - try { - // Update tasks into the ditto collection using DQL UPDATE statement - // https://docs.ditto.live/sdk/latest/crud/update#updating - DittoHelper.execute(ditto, "UPDATE tasks SET title=:title WHERE _id=:id", args); - } catch (Exception e) { - e.printStackTrace(); - } + TasksRepository.editTaskTitle(task, newTitle); } private void toggleTask(Task task) { - if (ditto == null) { - Log.i("MainActivity", "Ditto disabled - toggle task ignored: " + task.getTitle()); - return; - } - - Map args = new HashMap<>(); - args.put("id", task.getId()); - args.put("done", !task.isDone()); - - try { - // Update tasks into the ditto collection using DQL UPDATE statement - // https://docs.ditto.live/sdk/latest/crud/update#updating - DittoHelper.execute(ditto, "UPDATE tasks SET done=:done WHERE _id=:id", args); - } catch (Exception e) { - e.printStackTrace(); - } + TasksRepository.toggleTask(task); } private void deleteTask(Task task) { - if (ditto == null) { - Log.i("MainActivity", "Ditto disabled - delete task ignored: " + task.getTitle()); - return; - } - - Map args = Collections.singletonMap("id", task.getId()); - - try { - // UPDATE DQL Statement using Soft-Delete pattern - // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern - DittoHelper.execute(ditto, "UPDATE tasks SET deleted=true WHERE _id=:id", args); - } catch (Exception e) { - e.printStackTrace(); - } + TasksRepository.deleteTask(task); } private void toggleSync() { - if (ditto == null) { - return; - } - - boolean isSyncActive = DittoHelper.isSyncActive(ditto); + boolean isSyncActive = DittoManager.isSyncActive(); var nextColor = isSyncActive ? null : ColorStateList.valueOf(0xFFBB86FC); var nextText = isSyncActive ? "Sync Inactive" : "Sync Active"; @@ -282,15 +180,15 @@ private void toggleSync() { // https://docs.ditto.live/sdk/latest/sync/start-and-stop-sync try { if (isSyncActive) { - DittoHelper.stopSync(ditto); + DittoManager.stopSync(); } else { - DittoHelper.startSync(ditto); + DittoManager.startSync(); } syncSwitch.setChecked(!isSyncActive); syncSwitch.setTrackTintList(nextColor); syncSwitch.setText(nextText); } catch (Exception e) { - e.printStackTrace(); + Log.e("MainActivity", "Failed to toggle sync", e); } } diff --git a/android-java/app/src/main/java/com/example/dittotasks/TasksApplication.java b/android-java/app/src/main/java/com/example/dittotasks/TasksApplication.java new file mode 100644 index 000000000..a7c6b0cae --- /dev/null +++ b/android-java/app/src/main/java/com/example/dittotasks/TasksApplication.java @@ -0,0 +1,29 @@ +package com.example.dittotasks; + +import android.app.Application; +import android.util.Log; + +/** + * Creates the process-global Ditto instance exactly once, at app startup, and starts + * sync. Ditto lives for the whole process and is never tied to an Activity's lifecycle, + * so a configuration change such as rotation recreates the Activity without recreating + * Ditto (which would contend on the persistence-directory lock). + */ +public class TasksApplication extends Application { + private static final String TAG = "TasksApplication"; + + @Override + public void onCreate() { + super.onCreate(); + try { + DittoManager.initialize( + BuildConfig.DITTO_DATABASE_ID, + BuildConfig.DITTO_SERVER_URL, + BuildConfig.DITTO_DEVELOPMENT_TOKEN); + DittoManager.startSync(); + } catch (Throwable ex) { + Log.e(TAG, "Failed to initialize Ditto", ex); + throw ex; + } + } +} diff --git a/android-java/app/src/main/java/com/example/dittotasks/TasksRepository.kt b/android-java/app/src/main/java/com/example/dittotasks/TasksRepository.kt new file mode 100644 index 000000000..3bbcb3e38 --- /dev/null +++ b/android-java/app/src/main/java/com/example/dittotasks/TasksRepository.kt @@ -0,0 +1,123 @@ +package com.example.dittotasks + +import android.util.Log +import com.ditto.kotlin.DittoStoreObserver +import com.ditto.kotlin.DittoSyncSubscription +import kotlinx.coroutines.runBlocking +import java.util.function.Consumer + +/** + * Owns the "tasks" data concern: the sync subscription (which tasks sync to this + * device) and the task CRUD operations. It calls the real Ditto API directly through + * [DittoManager.ditto] — there is no pass-through/wrapper layer over Ditto. + * + * This is a process-global singleton so the subscription is registered once and lives + * for the whole app, independent of any Activity. The store observer, by contrast, + * belongs to the UI: [observeTasks] returns the observer handle for the caller to hold + * and close on view teardown (see [MainActivity.onDestroy]). + */ +object TasksRepository { + private var subscription: DittoSyncSubscription? = null + + // Register a subscription, which determines what data syncs to this device. Idempotent — + // registered once for the life of the app. + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + @JvmStatic + fun registerSubscription() { + if (subscription == null) { + subscription = DittoManager.ditto.sync.registerSubscription(SUBSCRIPTION_QUERY) + } + } + + // Observe the local store for changes (soft-deleted tasks excluded), delivering the + // visible task list to [onTasksChanged] on every change. The returned observer handle + // is owned by the caller (the UI) and must be closed on view teardown. + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + @JvmStatic + fun observeTasks( + onTasksChanged: Consumer<@JvmSuppressWildcards List> + ): DittoStoreObserver = + DittoManager.ditto.store.registerObserver(OBSERVER_QUERY) { result -> + Log.d(TAG, "Observer callback triggered with ${result.items.size} items") + val tasks = result.items.map { Task.fromQueryItem(it) } + onTasksChanged.accept(tasks) + } + + @JvmStatic + fun createTask(title: String) { + val task = mapOf( + "title" to title, + "done" to false, + "deleted" to false + ) + + try { + // Add tasks into the ditto collection using a DQL INSERT statement. + // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents + runBlocking { + DittoManager.ditto.store.execute( + "INSERT INTO tasks DOCUMENTS (:task)", + mapOf("task" to task) + ) + } + } catch (e: Exception) { + Log.e(TAG, "Task write failed", e) + } + } + + @JvmStatic + fun editTaskTitle(task: Task, newTitle: String) { + try { + // Update tasks in the ditto collection using a DQL UPDATE statement. + // https://docs.ditto.live/sdk/latest/crud/update#updating + runBlocking { + DittoManager.ditto.store.execute( + "UPDATE tasks SET title = :title WHERE _id = :id", + mapOf("id" to task.id, "title" to newTitle) + ) + } + } catch (e: Exception) { + Log.e(TAG, "Task write failed", e) + } + } + + @JvmStatic + fun toggleTask(task: Task) { + try { + // Update tasks in the ditto collection using a DQL UPDATE statement. + // https://docs.ditto.live/sdk/latest/crud/update#updating + runBlocking { + DittoManager.ditto.store.execute( + "UPDATE tasks SET done = :done WHERE _id = :id", + mapOf("id" to task.id, "done" to !task.isDone) + ) + } + } catch (e: Exception) { + Log.e(TAG, "Task write failed", e) + } + } + + @JvmStatic + fun deleteTask(task: Task) { + try { + // UPDATE DQL statement using the soft-delete pattern. + // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern + runBlocking { + DittoManager.ditto.store.execute( + "UPDATE tasks SET deleted = true WHERE _id = :id", + mapOf("id" to task.id) + ) + } + } catch (e: Exception) { + Log.e(TAG, "Task write failed", e) + } + } + + private const val TAG = "TasksRepository" + + // The subscription controls what syncs to this device; the observer reacts to + // changes in the local database, hiding soft-deleted tasks from the list. + private const val SUBSCRIPTION_QUERY = "SELECT * FROM tasks" + private const val OBSERVER_QUERY = + "SELECT * FROM tasks WHERE NOT deleted" +} diff --git a/android-java/app/src/main/res/layout/activity_main.xml b/android-java/app/src/main/res/layout/activity_main.xml index ef89ef04c..e79c06aad 100644 --- a/android-java/app/src/main/res/layout/activity_main.xml +++ b/android-java/app/src/main/res/layout/activity_main.xml @@ -30,8 +30,8 @@ android:textColor="#757575" /> Add a new todo Add Todo Ditto Tasks - App ID - Playground Token + Database ID + Development Token \ No newline at end of file diff --git a/android-java/gradle/libs.versions.toml b/android-java/gradle/libs.versions.toml index ef0498e31..ef5ac5232 100644 --- a/android-java/gradle/libs.versions.toml +++ b/android-java/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -ditto = "5.0.0" +ditto = "5.0.3" agp = "8.7.3" kotlin = "2.0.0" coreKtx = "1.10.1" diff --git a/android-kotlin/QuickStartTasks/app/build.gradle.kts b/android-kotlin/QuickStartTasks/app/build.gradle.kts index 6f7782f93..133ca506b 100644 --- a/android-kotlin/QuickStartTasks/app/build.gradle.kts +++ b/android-kotlin/QuickStartTasks/app/build.gradle.kts @@ -17,9 +17,9 @@ fun loadEnvProperties(): Properties { FileInputStream(envFile).use { properties.load(it) } } else { val requiredEnvVars = listOf( - "DITTO_APP_ID", - "DITTO_PLAYGROUND_TOKEN", - "DITTO_AUTH_URL" + "DITTO_DATABASE_ID", + "DITTO_DEVELOPMENT_TOKEN", + "DITTO_SERVER_URL" ) for (envVar in requiredEnvVars) { @@ -35,9 +35,9 @@ androidComponents { onVariants { val prop = loadEnvProperties() val buildConfigFields = mapOf( - "DITTO_APP_ID" to "Ditto application ID", - "DITTO_PLAYGROUND_TOKEN" to "Ditto playground token", - "DITTO_AUTH_URL" to "Ditto authentication URL", + "DITTO_DATABASE_ID" to "Ditto database ID", + "DITTO_DEVELOPMENT_TOKEN" to "Ditto development token", + "DITTO_SERVER_URL" to "Ditto server URL", "TEST_DOCUMENT_TITLE" to "Test document title for BrowserStack verification" ) diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoHandler.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoHandler.kt deleted file mode 100644 index 8b660b75a..000000000 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoHandler.kt +++ /dev/null @@ -1,20 +0,0 @@ -package live.ditto.quickstart.tasks - -import com.ditto.kotlin.* - -class DittoHandler { - companion object { - lateinit var ditto: Ditto - private set - - fun initialize(config: DittoConfig) { - if (::ditto.isInitialized) { - throw IllegalStateException("Ditto is already initialized") - } - ditto = DittoFactory.create(config = config) - } - - val isInitialized: Boolean - get() = ::ditto.isInitialized - } -} diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoManager.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoManager.kt new file mode 100644 index 000000000..edb07ce47 --- /dev/null +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoManager.kt @@ -0,0 +1,78 @@ +package live.ditto.quickstart.tasks + +import com.ditto.kotlin.Ditto +import com.ditto.kotlin.DittoAuthenticationProvider +import com.ditto.kotlin.DittoConfig +import com.ditto.kotlin.DittoFactory +import com.ditto.kotlin.DittoLog + +/** + * Owns configuration and lifecycle of the Ditto instance: creating it, wiring up + * development-mode authentication, and controlling sync. It knows nothing about + * tasks. + * + * It vends the configured Ditto instance (via [ditto]) so the rest of the app + * can use the real Ditto API directly. + */ +class DittoManager { + companion object { + private const val TAG = "DittoManager" + + lateinit var ditto: Ditto + private set + + val isInitialized: Boolean + get() = ::ditto.isInitialized + + /** + * Create and configure the Ditto instance and set up authentication. + * Does not start sync — call [startSync] for that. + */ + fun initialize(config: DittoConfig, developmentToken: String) { + if (::ditto.isInitialized) { + throw IllegalStateException("Ditto is already initialized") + } + ditto = DittoFactory.create(config = config) + setupAuthentication(developmentToken) + DittoLog.d(TAG, "Ditto instance created successfully") + } + + // Set the expiration handler before starting sync. + // https://docs.ditto.live/sdk/latest/sync/authentication + private fun setupAuthentication(token: String) { + ditto.auth?.let { auth -> + auth.expirationHandler = { ditto, _ -> + try { + val clientInfo = ditto.auth?.login( + token = token, + provider = DittoAuthenticationProvider.development() + ) + DittoLog.d(TAG, "Auth response: $clientInfo") + } catch (ex: Throwable) { + DittoLog.e(TAG, "Authentication failed: $ex") + } + } + } + DittoLog.d(TAG, "Ditto authentication setup complete") + } + + /** Start syncing data with other devices. */ + // https://docs.ditto.live/sdk/latest/sync/start-and-stop-sync + fun startSync() { + if (!ditto.sync.isActive) { + ditto.sync.start() + } + } + + /** Stop syncing data with other devices. */ + fun stopSync() { + if (ditto.sync.isActive) { + ditto.sync.stop() + } + } + + /** Whether sync is currently active. */ + val isSyncActive: Boolean + get() = ditto.sync.isActive + } +} diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt index 29393e39c..9efa4c741 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt @@ -2,7 +2,6 @@ package live.ditto.quickstart.tasks import android.app.Application import android.content.Context -import com.ditto.kotlin.DittoAuthenticationProvider import com.ditto.kotlin.DittoConfig import com.ditto.kotlin.DittoLog @@ -25,46 +24,19 @@ class TasksApplication : Application() { override fun onCreate() { super.onCreate() initializeDitto() - setupAuthentication() } private fun initializeDitto() { try { val config = DittoConfig( - databaseId = BuildConfig.DITTO_APP_ID, - connect = DittoConfig.Connect.Server(url = BuildConfig.DITTO_AUTH_URL) + databaseId = BuildConfig.DITTO_DATABASE_ID, + connect = DittoConfig.Connect.Server(url = BuildConfig.DITTO_SERVER_URL) ) - DittoHandler.initialize(config) - DittoLog.d(tag, "Ditto instance created successfully") + DittoManager.initialize(config, BuildConfig.DITTO_DEVELOPMENT_TOKEN) } catch (ex: Throwable) { DittoLog.e(tag, "Failed to initialize Ditto: $ex") throw ex } } - - private fun setupAuthentication() { - try { - val token = BuildConfig.DITTO_PLAYGROUND_TOKEN - - // Set the expiration handler before starting sync - // https://docs.ditto.live/sdk/latest/sync/authentication - DittoHandler.ditto.auth?.let { auth -> - auth.expirationHandler = { ditto, _ -> - try { - val clientInfo = ditto.auth?.login( - token = token, - provider = DittoAuthenticationProvider.development() - ) - DittoLog.d(tag, "Auth response: $clientInfo") - } catch (ex: Throwable) { - DittoLog.e(tag, "Authentication failed: $ex") - } - } - } - DittoLog.d(tag, "Ditto authentication setup complete") - } catch (ex: Throwable) { - DittoLog.e(tag, "Failed to setup authentication: $ex") - } - } } diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/data/TasksRepository.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/data/TasksRepository.kt new file mode 100644 index 000000000..69bdd87a2 --- /dev/null +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/data/TasksRepository.kt @@ -0,0 +1,98 @@ +package live.ditto.quickstart.tasks.data + +import com.ditto.kotlin.DittoSyncSubscription +import kotlinx.coroutines.flow.Flow +import live.ditto.quickstart.tasks.DittoManager + +/** + * Owns everything specific to the tasks data: the sync subscription, the store + * observer that streams the current task list, and the task CRUD operations. It + * talks to Ditto directly through the instance vended by [DittoManager]. + */ +object TasksRepository { + + // The subscription controls what syncs to this device and is written to the + // local database; the observer reacts to changes in that local database, + // hiding soft-deleted tasks from the displayed list. + private const val SUBSCRIPTION_QUERY = "SELECT * FROM tasks" + private const val OBSERVER_QUERY = + "SELECT * FROM tasks WHERE NOT deleted" + + private var syncSubscription: DittoSyncSubscription? = null + + // Observe the local store for changes, hiding soft-deleted tasks. + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + fun observeTasks(): Flow> = + DittoManager.ditto.store.observe(OBSERVER_QUERY) { result -> + result.items.map { item -> Task.fromJson(item.jsonString()) } + } + + // Register a subscription, which determines what data syncs to this peer. + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + fun registerSubscription() { + if (syncSubscription == null) { + syncSubscription = DittoManager.ditto.sync.registerSubscription(SUBSCRIPTION_QUERY) + } + } + + // Update tasks in the ditto collection using a DQL UPDATE statement. + // https://docs.ditto.live/sdk/latest/crud/update#updating + suspend fun toggle(task: Task) { + DittoManager.ditto.store.execute( + "UPDATE tasks SET done = :done WHERE _id = :id", + mapOf("done" to !task.done, "id" to task._id) + ) + } + + // UPDATE DQL statement using the soft-delete pattern. + // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern + suspend fun delete(taskId: String) { + DittoManager.ditto.store.execute( + "UPDATE tasks SET deleted = true WHERE _id = :id", + mapOf("id" to taskId) + ) + } + + // Load a single task by ID, used to pre-fill the edit form. + suspend fun getTask(taskId: String): Task? = + DittoManager.ditto.store.execute( + "SELECT * FROM tasks WHERE _id = :id", + mapOf("id" to taskId) + ) { result -> + result.items.firstOrNull()?.let { Task.fromJson(it.jsonString()) } + } + + // Add a task to the ditto collection using a DQL INSERT statement. + // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents + suspend fun insertTask(title: String, done: Boolean) { + DittoManager.ditto.store.execute( + "INSERT INTO tasks DOCUMENTS (:task)", + mapOf( + "task" to mapOf( + "title" to title, + "done" to done, + "deleted" to false + ) + ) + ) + } + + // Update the title and done state of an existing task. + // https://docs.ditto.live/sdk/latest/crud/update#updating + suspend fun updateTask(id: String, title: String, done: Boolean) { + DittoManager.ditto.store.execute( + """ + UPDATE tasks + SET + title = :title, + done = :done + WHERE _id = :id + """, + mapOf( + "title" to title, + "done" to done, + "id" to id + ) + ) + } +} diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt index c261d7bb9..38a483a54 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt @@ -7,14 +7,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import live.ditto.quickstart.tasks.DittoHandler -import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto -import live.ditto.quickstart.tasks.data.Task +import live.ditto.quickstart.tasks.DittoManager +import live.ditto.quickstart.tasks.data.TasksRepository class EditScreenViewModel : ViewModel() { init { - check(DittoHandler.isInitialized) { + check(DittoManager.isInitialized) { "Ditto must be initialized before ViewModels are created" } } @@ -48,14 +47,7 @@ class EditScreenViewModel : ViewModel() { viewModelScope.launch { try { - val task = ditto.store.execute( - "SELECT * FROM tasks WHERE _id = :_id AND NOT deleted", - mapOf("_id" to taskId) - ) { result -> - result.items.firstOrNull()?.let { Task.fromJson(it.jsonString()) } - } - - task?.let { + TasksRepository.getTask(taskId)?.let { _id = it._id _title.value = it.title _done.value = it.done @@ -71,39 +63,11 @@ class EditScreenViewModel : ViewModel() { try { val titleValue = _title.value val doneValue = _done.value - if (_id == null) { - // Add tasks into the ditto collection using DQL INSERT statement - // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents - ditto.store.execute( - "INSERT INTO tasks DOCUMENTS (:doc)", - mapOf( - "doc" to mapOf( - "title" to titleValue, - "done" to doneValue, - "deleted" to false - ) - ) - ) + val id = _id + if (id == null) { + TasksRepository.insertTask(titleValue, doneValue) } else { - // Update tasks in the ditto collection using DQL UPDATE statement - // https://docs.ditto.live/sdk/latest/crud/update#updating - _id?.let { id -> - ditto.store.execute( - """ - UPDATE tasks - SET - title = :title, - done = :done - WHERE _id = :id - AND NOT deleted - """, - mapOf( - "title" to titleValue, - "done" to doneValue, - "id" to id - ) - ) - } + TasksRepository.updateTask(id, titleValue, doneValue) } } catch (e: Throwable) { Log.e(TAG, "Unable to save task", e) @@ -112,16 +76,9 @@ class EditScreenViewModel : ViewModel() { } fun delete() { - // UPDATE DQL Statement using Soft-Delete pattern - // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern viewModelScope.launch { try { - _id?.let { id -> - ditto.store.execute( - "UPDATE tasks SET deleted = true WHERE _id = :id", - mapOf("id" to id) - ) - } + _id?.let { id -> TasksRepository.delete(id) } } catch (e: Throwable) { Log.e(TAG, "Unable to set deleted=true", e) } diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt index 505fa3622..694f6d446 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt @@ -71,14 +71,14 @@ fun TasksListScreen(navController: NavController) { Text( text = stringResource( id = R.string.tasks_app_id, - BuildConfig.DITTO_APP_ID + BuildConfig.DITTO_DATABASE_ID ), style = MaterialTheme.typography.labelSmall ) Text( text = stringResource( id = R.string.tasks_token, - BuildConfig.DITTO_PLAYGROUND_TOKEN + BuildConfig.DITTO_DEVELOPMENT_TOKEN ), style = MaterialTheme.typography.labelSmall ) @@ -192,7 +192,7 @@ fun TasksListScreen(navController: NavController) { @Composable fun TasksList( tasks: List, - onToggle: ((taskId: String) -> Unit)? = null, + onToggle: ((task: Task) -> Unit)? = null, onClickEdit: ((taskId: String) -> Unit)? = null, onClickDelete: ((taskId: String) -> Unit)? = null, ) { @@ -200,7 +200,7 @@ fun TasksList( items(tasks, key = { it._id }) { task -> TaskRow( task = task, - onToggle = { onToggle?.invoke(it._id) }, + onToggle = { onToggle?.invoke(it) }, onClickEdit = { onClickEdit?.invoke(it._id) }, onClickDelete = { onClickDelete?.invoke(it._id) } ) diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt index 90787b247..156e079c1 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt @@ -1,144 +1,67 @@ package live.ditto.quickstart.tasks.list -import android.content.Context import android.util.Log -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.preferencesDataStore import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ditto.kotlin.DittoSyncSubscription +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import live.ditto.quickstart.tasks.DittoHandler -import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto -import live.ditto.quickstart.tasks.TasksApplication +import live.ditto.quickstart.tasks.DittoManager import live.ditto.quickstart.tasks.data.Task - -// The value of the Sync switch is stored in persistent settings -private val Context.preferencesDataStore by preferencesDataStore("tasks_list_settings") -private val SYNC_ENABLED_KEY = booleanPreferencesKey("sync_enabled") +import live.ditto.quickstart.tasks.data.TasksRepository class TasksListScreenViewModel : ViewModel() { // Verify Ditto readiness before any property initializer below touches it. init { - check(DittoHandler.isInitialized) { + check(DittoManager.isInitialized) { "Ditto must be initialized before ViewModels are created" } } companion object { private const val TAG = "TasksListScreenViewModel" - - private const val QUERY = "SELECT * FROM tasks WHERE NOT deleted" } - private val preferencesDataStore = TasksApplication.applicationContext().preferencesDataStore - - // Use StateFlow with store.observe() for reactive updates - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - val tasks: StateFlow> = ditto.store.observe(QUERY) { result -> - result.items.map { item -> Task.fromJson(item.jsonString()) } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) + // The repository owns the observer; the ViewModel exposes it as UI state. + val tasks: StateFlow> = TasksRepository.observeTasks() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) - // Derive sync state directly from DataStore so the UI reflects the persisted value - // as soon as it loads, instead of flashing a hardcoded default first. - val syncEnabled: StateFlow = preferencesDataStore.data - .map { prefs -> prefs[SYNC_ENABLED_KEY] ?: true } - .stateIn(viewModelScope, SharingStarted.Eagerly, true) - - private var syncSubscription: DittoSyncSubscription? = null + // Sync is on by default so the app syncs out of the box. This is in-memory + // session state — the toggle is not persisted across launches. + private val _syncEnabled = MutableStateFlow(true) + val syncEnabled: StateFlow = _syncEnabled.asStateFlow() init { - viewModelScope.launch { populateTasksCollection() } - - // Apply the persisted sync preference whenever it changes. - viewModelScope.launch { - preferencesDataStore.data - .map { prefs -> prefs[SYNC_ENABLED_KEY] ?: true } - .distinctUntilChanged() - .collect { enabled -> applySyncState(enabled) } - } + // Start sync on launch to match the default enabled state. + applySyncState(_syncEnabled.value) } fun setSyncEnabled(enabled: Boolean) { - viewModelScope.launch { - preferencesDataStore.edit { settings -> - settings[SYNC_ENABLED_KEY] = enabled - } - } + _syncEnabled.value = enabled + applySyncState(enabled) } private fun applySyncState(enabled: Boolean) { - if (enabled && !ditto.sync.isActive) { - try { - // Starting sync - // https://docs.ditto.live/sdk/latest/sync/start-and-stop-sync - ditto.sync.start() - - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - syncSubscription = ditto.sync.registerSubscription(QUERY) - } catch (e: Throwable) { - Log.e(TAG, "Unable to start sync", e) - } - } else if (!enabled && ditto.sync.isActive) { - try { - syncSubscription?.close() - syncSubscription = null - ditto.sync.stop() - } catch (e: Throwable) { - Log.e(TAG, "Unable to stop sync", e) - } - } - } - - // Add initial tasks to the collection if they have not already been added. - private suspend fun populateTasksCollection() { - val tasks = listOf( - Task("50191411-4C46-4940-8B72-5F8017A04FA7", "Buy groceries"), - Task("6DA283DA-8CFE-4526-A6FA-D385089364E5", "Clean the kitchen"), - Task("5303DDF8-0E72-4FEB-9E82-4B007E5797F0", "Schedule dentist appointment"), - Task("38411F1B-6B49-4346-90C3-0B16CE97E174", "Pay bills") - ) - - tasks.forEach { task -> - try { - // Add tasks into the ditto collection using DQL INSERT statement - // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents - ditto.store.execute( - "INSERT INTO tasks INITIAL DOCUMENTS (:task)", - mapOf("task" to task.toMap()) - ) - } catch (e: Throwable) { - Log.e(TAG, "Unable to insert initial document", e) + try { + if (enabled) { + DittoManager.startSync() + TasksRepository.registerSubscription() + } else { + DittoManager.stopSync() } + } catch (e: Throwable) { + Log.e(TAG, "Unable to apply sync state (enabled=$enabled)", e) } } - fun toggle(taskId: String) { + fun toggle(task: Task) { viewModelScope.launch { try { - val task = ditto.store.execute( - "SELECT * FROM tasks WHERE _id = :_id AND NOT deleted", - mapOf("_id" to taskId) - ) { result -> - result.items.firstOrNull()?.let { Task.fromJson(it.jsonString()) } - } - - task?.let { - // Update tasks in the ditto collection using DQL UPDATE statement - // https://docs.ditto.live/sdk/latest/crud/update#updating - ditto.store.execute( - "UPDATE tasks SET done = :toggled WHERE _id = :_id AND NOT deleted", - mapOf("toggled" to !it.done, "_id" to taskId) - ) - } + TasksRepository.toggle(task) } catch (e: Throwable) { Log.e(TAG, "Unable to toggle done state", e) } @@ -148,12 +71,7 @@ class TasksListScreenViewModel : ViewModel() { fun delete(taskId: String) { viewModelScope.launch { try { - // UPDATE DQL Statement using Soft-Delete pattern - // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern - ditto.store.execute( - "UPDATE tasks SET deleted = true WHERE _id = :id", - mapOf("id" to taskId) - ) + TasksRepository.delete(taskId) } catch (e: Throwable) { Log.e(TAG, "Unable to set deleted=true", e) } diff --git a/android-kotlin/QuickStartTasks/gradle/libs.versions.toml b/android-kotlin/QuickStartTasks/gradle/libs.versions.toml index e14417738..4ee61a86c 100644 --- a/android-kotlin/QuickStartTasks/gradle/libs.versions.toml +++ b/android-kotlin/QuickStartTasks/gradle/libs.versions.toml @@ -15,7 +15,7 @@ appcompat = "1.7.1" datastorePreferences = "1.2.1" koin-bom = "4.2.1" coroutines-tests = "1.10.2" -ditto = "5.0.0" +ditto = "5.0.3" monitor = "1.8.0" json = "20240303" diff --git a/android-kotlin/README.md b/android-kotlin/README.md index 515577b59..24ee4309f 100644 --- a/android-kotlin/README.md +++ b/android-kotlin/README.md @@ -13,7 +13,7 @@ After you have completed the [common prerequisites] you will need the following: ## Documentation - [Kotlin Install Guide](https://docs.ditto.live/install-guides/kotlin) -- [Kotlin API Reference](https://software.ditto.live/android/Ditto/5.0.0/api-reference/) +- [Kotlin API Reference](https://docs.ditto.live/sdk/latest/api-reference/kotlin) - [Kotlin SDK Release Notes](https://docs.ditto.live/release-notes/kotlin) [common prerequisites]: https://github.com/getditto/quickstart#common-prerequisites @@ -23,8 +23,8 @@ After you have completed the [common prerequisites] you will need the following: Assuming you have Android Studio and other prerequisites installed, you can build and run the app by following these steps: -1. Create an application at . Make note of the database ID (used to be called app ID) and online playground token. -2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID (used to be called App ID), Online Playground Token, and Auth URL. +1. Create an application at . Make note of the database ID and development token. +2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID, Development Token, and Server URL. 3. Launch Android Studio and open the `quickstart/android-kotlin` directory. 4. In Android Studio, select a connected Android device, or create and launch an Android emulator and select it as the destination, then choose the **Run > Run 'app'** menu item. @@ -55,21 +55,19 @@ Android Studio to automatically download the Ditto SDK from Maven Central and add it to the project: ```kotlin - implementation("live.ditto:ditto:4.11.1") + implementation(libs.com.ditto) ``` -To use a newer version of the SDK, change the version number in this line. - ### Initializing Ditto In `app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt`, you will see the `TasksApplication` class. This class is a subclass of `Application`. Its `onCreate()` method, which is called by the system when the app is launched, -calls `setupDitto()`, which gets application ID and online playground token from +calls `setupDitto()`, which gets Database ID and development token from the build configuration and uses it to create an instance of the `Ditto` class -which is stored in a singleton object of the `DittoHandler` class. +which is stored in a singleton object of the `DittoManager` class. -Other classes import `live.ditto.quickstart.tasks.DittoHandler.Companion.ditto` +Other classes import `live.ditto.quickstart.tasks.DittoManager.Companion.ditto` to access the singleton `Ditto` instance. ### The Task Data Model diff --git a/.github/assets/js-web-ditto-screenshot.png b/assets/js-web-ditto-screenshot.png similarity index 100% rename from .github/assets/js-web-ditto-screenshot.png rename to assets/js-web-ditto-screenshot.png diff --git a/cpp-tui/README.md b/cpp-tui/README.md index b34c50949..7a2b6ef62 100644 --- a/cpp-tui/README.md +++ b/cpp-tui/README.md @@ -21,7 +21,7 @@ The build system will automatically download the Ditto C++ SDK when you run `mak ## Documentation - [C++ Install Guide](https://docs.ditto.live/install-guides/cpp) -- [C++ API Reference](https://software.ditto.live/cpp/Ditto/4.11.0/api-reference/) +- [C++ API Reference](https://docs.ditto.live/sdk/latest/api-reference/cpp) - [C++ Release Notes](https://docs.ditto.live/release-notes/cpp) [common prerequisites]: https://github.com/getditto/quickstart#common-prerequisites @@ -30,8 +30,8 @@ The build system will automatically download the Ditto C++ SDK when you run `mak Assuming you have the prerequisites installed, you can build and run the app by following these steps: -1. Create an application at . Make note of the app ID and online playground token. -2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your app ID and online playground token. +1. Create an application at . Make note of the Database ID and development token. +2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID and development token. 3. In a shell, navigate to the `quickstart/cpp-tui/taskscpp` directory and run the command `make build` to automatically download the Ditto SDK and build the C++ application. The build system will detect your Linux architecture (x86_64/arm64) and download the appropriate Ditto C++ SDK version automatically. diff --git a/cpp-tui/taskscpp/CMakeLists.txt b/cpp-tui/taskscpp/CMakeLists.txt index ef745ff15..2709623d9 100644 --- a/cpp-tui/taskscpp/CMakeLists.txt +++ b/cpp-tui/taskscpp/CMakeLists.txt @@ -88,7 +88,8 @@ if(BUILD_TESTS) add_executable(integration_test tests/integration_test.cpp src/task.cpp - src/tasks_peer.cpp + src/ditto_manager.cpp + src/tasks_repository.cpp src/tasks_log.cpp ) target_include_directories(integration_test PRIVATE src sdk third_party/cxxopts/include) diff --git a/cpp-tui/taskscpp/Makefile b/cpp-tui/taskscpp/Makefile index fc2e95b7e..de6a9c320 100644 --- a/cpp-tui/taskscpp/Makefile +++ b/cpp-tui/taskscpp/Makefile @@ -2,7 +2,7 @@ BUILD_TYPE ?= Debug # Ditto SDK version and platform detection -DITTO_SDK_VERSION ?= 4.14.3 +DITTO_SDK_VERSION ?= 5.0.3 PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]') ARCH := $(shell uname -m) diff --git a/cpp-tui/taskscpp/src/ditto_manager.cpp b/cpp-tui/taskscpp/src/ditto_manager.cpp new file mode 100644 index 000000000..8d05f72aa --- /dev/null +++ b/cpp-tui/taskscpp/src/ditto_manager.cpp @@ -0,0 +1,124 @@ +#include "ditto_manager.h" +#include "tasks_log.h" + +#include "Ditto.h" + +#include +#include +#include + +using namespace std; + +/// Initialize a Ditto instance. +static shared_ptr init_ditto(string database_id, + string development_token, + string server_url, + string persistence_dir) { + try { + auto config = ditto::DittoConfig::default_config() + .set_database_id(std::move(database_id)) + .set_persistence_directory(std::move(persistence_dir)) + .set_server_connect(std::move(server_url)); + + auto ditto = ditto::Ditto::open(std::move(config)); + + // Log in with the development token using the built-in development + // authentication provider. The expiration handler re-authenticates + // automatically when the credential is about to expire. + const auto provider = ditto::Authenticator::get_development_provider(); + if (auto auth = ditto->get_auth()) { + auth->set_expiration_handler( + [development_token, provider](ditto::Ditto &d, uint32_t) { + if (auto a = d.get_auth()) { + a->login(development_token, provider, + [](std::unique_ptr, + std::unique_ptr err) { + if (err) { + log_error("Failed to re-authenticate: " + + string(err->what())); + } + }); + } + }); + + auth->login(development_token, provider, + [](std::unique_ptr, + std::unique_ptr err) { + if (err) { + log_error("Failed to authenticate: " + + string(err->what())); + } + }); + } + + return ditto; + } catch (const exception &err) { + throw runtime_error("unable to initialize Ditto: " + string(err.what())); + } +} + +// Private implementation of the DittoManager class. +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) +class DittoManager::Impl { +public: + shared_ptr ditto; + + Impl(string database_id, string development_token, string server_url, + string persistence_dir) + : ditto(init_ditto(std::move(database_id), std::move(development_token), + std::move(server_url), std::move(persistence_dir))) {} + + ~Impl() noexcept { + try { + stop_sync(); + } catch (const exception &err) { + std::cerr << "Failed to destroy Ditto manager instance: " + + string(err.what()) + << std::endl; + } + } + + void start_sync() { + if (is_sync_active()) { + return; + } + + ditto->get_sync().start(); + } + + void stop_sync() { + if (!is_sync_active()) { + return; + } + + ditto->get_sync().stop(); + } + + bool is_sync_active() const { return ditto->get_sync().is_active(); } + +}; // class DittoManager::Impl + +DittoManager::DittoManager(string database_id, string development_token, + string server_url, string persistence_dir) + : impl(std::make_shared( + std::move(database_id), std::move(development_token), + std::move(server_url), std::move(persistence_dir))) {} + +DittoManager::~DittoManager() noexcept { + try { + log_debug("Destroying DittoManager instance"); + } catch (const std::exception &) { // NOLINT(bugprone-empty-catch) + } +} + +void DittoManager::start_sync() { impl->start_sync(); } + +void DittoManager::stop_sync() { impl->stop_sync(); } + +bool DittoManager::is_sync_active() const { return impl->is_sync_active(); } + +shared_ptr DittoManager::get_ditto() const { return impl->ditto; } + +string DittoManager::get_ditto_sdk_version() { + return ditto::Ditto::get_version(); +} diff --git a/cpp-tui/taskscpp/src/ditto_manager.h b/cpp-tui/taskscpp/src/ditto_manager.h new file mode 100644 index 000000000..2caf90a92 --- /dev/null +++ b/cpp-tui/taskscpp/src/ditto_manager.h @@ -0,0 +1,54 @@ +#ifndef DITTO_QUICKSTART_DITTO_MANAGER_H +#define DITTO_QUICKSTART_DITTO_MANAGER_H + +#include +#include + +#include "Ditto.h" + +/// Owns everything about configuring and running the Ditto instance: opening +/// it, wiring up development-mode authentication, and controlling sync. It +/// knows nothing about tasks. +/// +/// It vends the configured Ditto instance (via `get_ditto()`) so the rest of +/// the app can use the real Ditto API directly. +class DittoManager { +public: + /// Returns a string identifying the version of the Ditto SDK. + static std::string get_ditto_sdk_version(); + + /// Open and configure a Ditto instance. Does not start sync; call + /// `start_sync()` for that. + DittoManager(std::string database_id, std::string ditto_development_token, + std::string server_url, std::string ditto_persistence_dir); + + virtual ~DittoManager() noexcept; + + // DittoManager owns the underlying Ditto instance and its sync lifecycle, so + // it is non-copyable and non-movable. Share it via `std::shared_ptr` when + // more than one component needs it (see TasksRepository). + DittoManager(const DittoManager &) = delete; + DittoManager(DittoManager &&) = delete; + + DittoManager &operator=(const DittoManager &) = delete; + DittoManager &operator=(DittoManager &&) = delete; + + /// Start syncing data with other devices. + void start_sync(); + + /// Stop syncing data with other devices. + void stop_sync(); + + /// Return true if currently syncing with other devices. + bool is_sync_active() const; + + /// The configured Ditto instance, so callers can use the real Ditto API + /// directly. + std::shared_ptr get_ditto() const; + +private: + class Impl; // private implementation class ("pimpl pattern") + std::shared_ptr impl; +}; + +#endif // DITTO_QUICKSTART_DITTO_MANAGER_H diff --git a/cpp-tui/taskscpp/src/main.cpp b/cpp-tui/taskscpp/src/main.cpp index ac2dc4dea..1078e3bc8 100644 --- a/cpp-tui/taskscpp/src/main.cpp +++ b/cpp-tui/taskscpp/src/main.cpp @@ -1,8 +1,9 @@ #include "env.h" +#include "ditto_manager.h" #include "task.h" #include "tasks_log.h" -#include "tasks_peer.h" +#include "tasks_repository.h" #ifdef DITTO_QUICKSTART_TUI #include "tasks_tui.h" @@ -77,11 +78,7 @@ int main(int argc, const char *argv[]) { ("d,delete", "Delete a task", cxxopts::value>(), "TASK_ID") ("l,list", "List tasks") - ("list-all", "List all tasks, including those marked deleted") - ("m,monitor", "Monitor tasks for changes") - ("cleanup", "Evict all deleted tasks from local store") - ("query", "Run a DQL query using the peer's Ditto instance", - cxxopts::value>(), "STRING"); + ("m,monitor", "Monitor tasks for changes"); options.add_options("Sync") ("pre", "Number of seconds to synchronize before the operation", @@ -90,15 +87,12 @@ int main(int argc, const char *argv[]) { cxxopts::value()->default_value("5"), "N") ("p,persistence-directory", "Persistence directory", cxxopts::value(), "PATH") - ("app-id", "Ditto App ID", - cxxopts::value(), "APP_ID") - ("online-playground-token", "Ditto Online Playground token", + ("database-id", "Ditto Database ID", + cxxopts::value(), "DATABASE_ID") + ("development-token", "Ditto Development token", cxxopts::value(), "TOKEN") - ("websocket-url", "Ditto WebSocket URL", - cxxopts::value(), "WEBSOCKET_URL") - ("auth-url", "Ditto Auth URL", - cxxopts::value(), "AUTH_URL") - ("enable-cloud-sync", "Enable cloud synchronization"); + ("server-url", "Ditto Server URL", + cxxopts::value(), "SERVER_URL"); options.add_options("Logging") ("q,quiet", "Disable non-logging output") @@ -107,8 +101,6 @@ int main(int argc, const char *argv[]) { ("info", "Info-level logging") ("debug", "Debug-level logging") ("v,verbose","Trace-level logging") - ("log","Log file output path", - cxxopts::value(), "PATH") ("export", "Export-log file path", cxxopts::value(), "PATH") ("ditto-sdk-version", "Print the Ditto SDK version"); @@ -117,10 +109,9 @@ int main(int argc, const char *argv[]) { const auto opt_parse = options.parse(argc, argv); // If no other commands are specified, then "tui" is the default behavior. - const vector commands{"add", "complete", "incomplete", - "title", "delete", "list", - "list-all", "monitor", "cleanup", - "query", "toggle", "ditto-sdk-version"}; + const vector commands{"add", "complete", "incomplete", + "title", "delete", "list", + "monitor", "toggle", "ditto-sdk-version"}; bool found_non_tui_command = false; for (const auto &command : commands) { if (opt_parse.count(command) > 0) { @@ -145,7 +136,7 @@ int main(int argc, const char *argv[]) { #endif if (opt_parse.count("ditto-sdk-version") > 0) { - cout << "Ditto SDK version: " << TasksPeer::get_ditto_sdk_version() + cout << "Ditto SDK version: " << DittoManager::get_ditto_sdk_version() << endl; exit(EXIT_SUCCESS); } @@ -169,10 +160,6 @@ int main(int argc, const char *argv[]) { } set_minimum_log_level(log_level); - if (opt_parse.count("log") > 0) { - set_log_file(opt_parse["log"].as()); - } - if (opt_parse.count("export") > 0) { export_log_path = opt_parse["export"].as(); } @@ -184,47 +171,44 @@ int main(int argc, const char *argv[]) { opt_parse.count("persistence-directory") > 0 ? opt_parse["persistence-directory"].as() : ""; - const auto app_id = opt_parse.count("app-id") > 0 - ? opt_parse["app-id"].as() - : DITTO_APP_ID; - const auto online_playground_token = - opt_parse.count("online-playground-token") > 0 - ? opt_parse["online-playground-token"].as() - : DITTO_PLAYGROUND_TOKEN; - const auto websocket_url = opt_parse.count("websocket-url") > 0 - ? opt_parse["websocket-url"].as() - : DITTO_WEBSOCKET_URL; - const auto auth_url = opt_parse.count("auth-url") > 0 - ? opt_parse["auth-url"].as() - : DITTO_AUTH_URL; - - const auto enable_cloud_sync = opt_parse.count("enable-cloud-sync") > 0; + const auto database_id = opt_parse.count("database-id") > 0 + ? opt_parse["database-id"].as() + : DITTO_DATABASE_ID; + const auto development_token = + opt_parse.count("development-token") > 0 + ? opt_parse["development-token"].as() + : DITTO_DEVELOPMENT_TOKEN; + const auto server_url = opt_parse.count("server-url") > 0 + ? opt_parse["server-url"].as() + : DITTO_SERVER_URL; const auto quiet = opt_parse["quiet"].as(); // Set this true if we make modifications and need to allow post-sync time. bool need_post_sync = false; - // The peer is destroyed at the end of this scope + // The manager and repository are destroyed at the end of this scope. The + // manager is held by shared_ptr so the repository (which shares ownership) + // can never outlive it. { - TasksPeer peer(app_id, online_playground_token, websocket_url, auth_url, - enable_cloud_sync, persistence_dir); - peer.insert_initial_tasks(); - peer.start_sync(); + auto manager = std::make_shared( + database_id, development_token, server_url, persistence_dir); + TasksRepository repository(manager); + manager->start_sync(); #ifdef DITTO_QUICKSTART_TUI if (found_tui_command || !found_non_tui_command) { - TasksTui tui(peer); + TasksTui tui(repository, manager); tui.run(); } else #endif { - // A thread must hold mtx while using peer or writing output. + // A thread must hold mtx while using the repository or writing output. mutex mtx; shared_ptr tasks_observer; if (opt_parse.count("monitor") > 0) { - tasks_observer = peer.register_tasks_observer( + tasks_observer = repository.register_tasks_observer( [quiet, &mtx](const vector &tasks) { if (!quiet && !tasks.empty()) { lock_guard lock(mtx); @@ -255,7 +239,7 @@ int main(int argc, const char *argv[]) { } lock_guard lock(mtx); - const auto task_id = peer.add_task(title, false); + const auto task_id = repository.add_task(title, false); if (!quiet) { cout << "Added task: " << task_id << ": " << title << endl; @@ -274,8 +258,9 @@ int main(int argc, const char *argv[]) { validate_task_substring(task_id_substring); lock_guard lock(mtx); - const auto task = peer.find_matching_task(task_id_substring); - peer.mark_task_complete(task._id, true); + const auto task = + repository.find_matching_task(task_id_substring); + repository.mark_task_complete(task._id, true); if (!quiet) { cout << "Marked task complete: " << task._id << endl; @@ -295,8 +280,9 @@ int main(int argc, const char *argv[]) { validate_task_substring(task_id_substring); lock_guard lock(mtx); - const auto task = peer.find_matching_task(task_id_substring); - peer.mark_task_complete(task._id, false); + const auto task = + repository.find_matching_task(task_id_substring); + repository.mark_task_complete(task._id, false); if (!quiet) { cout << "Marked task incomplete: " << task._id << endl; @@ -316,8 +302,9 @@ int main(int argc, const char *argv[]) { validate_task_substring(task_id_substring); lock_guard lock(mtx); - const auto task = peer.find_matching_task(task_id_substring); - peer.mark_task_complete(task._id, !task.done); + const auto task = + repository.find_matching_task(task_id_substring); + repository.mark_task_complete(task._id, !task.done); if (!quiet) { cout << "Toggled task completion: " << task._id << endl; @@ -349,8 +336,9 @@ int main(int argc, const char *argv[]) { } lock_guard lock(mtx); - const auto task = peer.find_matching_task(task_id_substring); - peer.update_task_title(task._id, title); + const auto task = + repository.find_matching_task(task_id_substring); + repository.update_task_title(task._id, title); if (!quiet) { cout << "Changed title of " << task._id << " to '" << title @@ -370,8 +358,9 @@ int main(int argc, const char *argv[]) { validate_task_substring(task_id_substring); lock_guard lock(mtx); - const auto task = peer.find_matching_task(task_id_substring); - peer.delete_task(task._id); + const auto task = + repository.find_matching_task(task_id_substring); + repository.delete_task(task._id); if (!quiet) { cout << "Deleted task: " << task._id << endl; @@ -383,38 +372,9 @@ int main(int argc, const char *argv[]) { } } - if (opt_parse.count("cleanup") > 0) { - need_post_sync = true; - try { - lock_guard lock(mtx); - peer.evict_deleted_tasks(); - if (!quiet) { - cout << "Evicted all deleted tasks" << endl; - } - } catch (const exception &err) { - cerr << "error: cleanup: " << err.what() << endl; - } - } - - if (opt_parse.count("query") > 0) { - need_post_sync = true; - for (const auto &query : opt_parse["query"].as>()) { - try { - lock_guard lock(mtx); - const auto result = peer.execute_dql_query(query); - if (!quiet) { - cout << "[" << query << "] result: \n" << result << endl; - } - } catch (const exception &err) { - cerr << "error: query [" << query << "]: " << err.what() << endl; - } - } - } - - auto include_deleted_tasks = opt_parse.count("list-all") > 0; - if (opt_parse.count("list") > 0 || opt_parse.count("list-all") > 0) { + if (opt_parse.count("list") > 0) { lock_guard lock(mtx); - auto tasks = peer.get_tasks(include_deleted_tasks); + auto tasks = repository.get_tasks(); if (tasks.empty()) { if (!quiet) { cout << "No tasks found" << endl; @@ -460,8 +420,8 @@ int main(int argc, const char *argv[]) { tasks_observer.reset(); } // !found_tui_command - peer.stop_sync(); - } // peer destroyed + manager->stop_sync(); + } // repository and manager destroyed if (!export_log_path.empty()) { export_log(export_log_path); diff --git a/cpp-tui/taskscpp/src/tasks_log.cpp b/cpp-tui/taskscpp/src/tasks_log.cpp index 71da24384..74ac0823c 100644 --- a/cpp-tui/taskscpp/src/tasks_log.cpp +++ b/cpp-tui/taskscpp/src/tasks_log.cpp @@ -1,37 +1,33 @@ #include "tasks_log.h" -// These functions are all thin wrappers over the ditto::Log() API. Application -// code should call these rather than the ditto::Log() API to make it easy to -// change or extend the logging implementation. +// These functions are all thin wrappers over the ditto::Logger API. +// Application code should call these rather than the ditto::Logger API to make +// it easy to change or extend the logging implementation. -void log_error(const std::string &msg) { ditto::Log::e(msg); } +void log_error(const std::string &msg) { ditto::Logger::e(msg); } -void log_warning(const std::string &msg) { ditto::Log::w(msg); } +void log_warning(const std::string &msg) { ditto::Logger::w(msg); } -void log_info(const std::string &msg) { ditto::Log::i(msg); } +void log_info(const std::string &msg) { ditto::Logger::i(msg); } -void log_debug(const std::string &msg) { ditto::Log::d(msg); } +void log_debug(const std::string &msg) { ditto::Logger::d(msg); } -void log_verbose(const std::string &msg) { ditto::Log::v(msg); } +void log_verbose(const std::string &msg) { ditto::Logger::v(msg); } -bool get_logging_enabled() { return ditto::Log::get_logging_enabled(); } +bool get_logging_enabled() { return ditto::Logger::get_logging_enabled(); } void set_logging_enabled(bool enabled) { - ditto::Log::set_logging_enabled(enabled); + ditto::Logger::set_logging_enabled(enabled); } ditto::LogLevel get_minimum_log_level() { - return ditto::Log::get_minimum_log_level(); + return ditto::Logger::get_minimum_log_level(); } void set_minimum_log_level(ditto::LogLevel level) { - ditto::Log::set_minimum_log_level(level); + ditto::Logger::set_minimum_log_level(level); } -void set_log_file(const std::string &path) { ditto::Log::set_log_file(path); } - -void disable_log_file() { ditto::Log::disable_log_file(); } - void export_log(const std::string &path) { - ditto::Log::export_to_file(path).get(); + ditto::Logger::export_to_file(path).get(); } diff --git a/cpp-tui/taskscpp/src/tasks_log.h b/cpp-tui/taskscpp/src/tasks_log.h index 11d8b9616..1e0781851 100644 --- a/cpp-tui/taskscpp/src/tasks_log.h +++ b/cpp-tui/taskscpp/src/tasks_log.h @@ -17,9 +17,6 @@ void set_logging_enabled(bool enabled); ditto::LogLevel get_minimum_log_level(); void set_minimum_log_level(ditto::LogLevel level); -void set_log_file(const std::string &path); -void disable_log_file(); - void export_log(const std::string &path); #endif // DITTO_QUICKSTART_TASKS_LOG_H diff --git a/cpp-tui/taskscpp/src/tasks_peer.cpp b/cpp-tui/taskscpp/src/tasks_peer.cpp deleted file mode 100644 index c155c0591..000000000 --- a/cpp-tui/taskscpp/src/tasks_peer.cpp +++ /dev/null @@ -1,455 +0,0 @@ -#include "tasks_peer.h" -#include "tasks_log.h" -#include "transform_container.h" - -#include "Ditto.h" - -#include -#include -#include -#include -#include - -using namespace std; -using json = nlohmann::json; - -/// Extract a Task object from a QueryResultItem. -static Task task_from(const ditto::QueryResultItem &item) { - return json::parse(item.json_string()).template get(); -} - -/// Convert a QueryResult to a collection of Task objects. -static vector tasks_from(const ditto::QueryResult &result) { - const auto item_count = result.item_count(); - vector tasks; - tasks.reserve(item_count); - for (size_t i = 0; i < item_count; ++i) { - tasks.emplace_back(task_from(result.get_item(i))); - } - return tasks; -} - -/// Convert a QueryResult to a JSON string -static string to_json_string(const ditto::QueryResult &result) { - const auto items = transform_container>( - result.items(), - [](const ditto::QueryResultItem &item) { return item.json_string(); }); - - const auto modified_document_ids = result.mutated_document_ids(); - - nlohmann::json result_json; - result_json["items"] = items; - result_json["modified_document_ids"] = modified_document_ids; - return result_json.dump(); -} - -/// Initialize a Ditto instance. -static shared_ptr -init_ditto(string app_id, string online_playground_token, string websocket_url, - string auth_url, bool enable_cloud_sync, string persistence_dir) { - try { - const auto identity = ditto::Identity::OnlinePlayground( - std::move(app_id), std::move(online_playground_token), - enable_cloud_sync, std::move(auth_url)); - - auto ditto = - std::make_shared(identity, std::move(persistence_dir)); - - ditto->update_transport_config( - [websocket_url](ditto::TransportConfig &config) { - config.enable_all_peer_to_peer(); - config.connect.websocket_urls.insert(websocket_url); - }); - - // Required for compatibility with DQL. - ditto->disable_sync_with_v3(); - - // Disable DQL strict mode - // https://docs.ditto.live/dql/strict-mode - const auto disableStrictModeCommand = - "ALTER SYSTEM SET DQL_STRICT_MODE = false"; - const auto result = ditto->get_store().execute(disableStrictModeCommand); - - return ditto; - } catch (const exception &err) { - throw runtime_error("unable to initialize Ditto: " + string(err.what())); - } -} - -// Private implementation of the TasksPeer class. -class TasksPeer::Impl { // NOLINT(cppcoreguidelines-special-member-functions) -private: - shared_ptr mtx; - shared_ptr ditto; - shared_ptr tasks_subscription; - - string select_tasks_query(bool include_deleted_tasks = false) { - if (include_deleted_tasks) { - return "SELECT * FROM tasks ORDER BY title ASC"; - } else { - return "SELECT * FROM tasks WHERE NOT deleted ORDER BY title ASC"; - } - } - -public: - Impl(string app_id, string online_playground_token, string websocket_url, - string auth_url, bool enable_cloud_sync, string persistence_dir) - : mtx(new mutex()), - ditto(init_ditto(std::move(app_id), std::move(online_playground_token), - std::move(websocket_url), std::move(auth_url), - enable_cloud_sync, // This is required to be set to - // false to use the correct URLs - std::move(persistence_dir))) {} - - ~Impl() noexcept { - try { - stop_sync(); - } catch (const exception &err) { - std::cerr << "Failed to destroy tasks peer instance: " + - string(err.what()) - << std::endl; - } - } - - void start_sync() { - if (is_sync_active()) { - return; - } - - ditto->start_sync(); - tasks_subscription = - ditto->get_sync().register_subscription("SELECT * FROM tasks"); - } - - void stop_sync() { - if (!is_sync_active()) { - return; - } - - tasks_subscription->cancel(); - tasks_subscription.reset(); - ditto->stop_sync(); - } - - bool is_sync_active() const { return ditto->get_is_sync_active(); } - - string add_task(const string &title, bool done) { - try { - const json task_args = { - {"title", title}, {"done", done}, {"deleted", false}}; - const auto command = "INSERT INTO tasks DOCUMENTS (:newTask)"; - const auto result = - ditto->get_store().execute(command, {{"newTask", task_args}}); - auto task_id = result.mutated_document_ids()[0]; - log_debug("Added task: " + task_id.to_string()); - return task_id.to_string(); - } catch (const exception &err) { - log_error("Failed to add task: " + string(err.what())); - throw runtime_error("unable to add task: " + string(err.what())); - } - } - - vector get_tasks(bool include_deleted_tasks) { - try { - const auto result = - ditto->get_store().execute(select_tasks_query(include_deleted_tasks)); - const auto tasks = tasks_from(result); - log_debug("Retrieved tasks; count=" + to_string(tasks.size())); - return tasks; - } catch (const exception &err) { - log_error("Failed to get tasks: " + string(err.what())); - throw runtime_error("unable to get tasks: " + string(err.what())); - } - } - - Task get_task(const string &task_id) { - try { - lock_guard lock(*mtx); - - if (task_id.empty()) { - throw invalid_argument("task_id must not be empty"); - } - const auto query = "SELECT * FROM tasks WHERE _id = :id AND NOT deleted"; - const auto result = ditto->get_store().execute(query, {{"id", task_id}}); - const auto item_count = result.item_count(); - if (item_count == 0) { - throw runtime_error(string("no tasks found with id \"") + task_id + - "\""); - } else if (item_count > 1) { - throw runtime_error("more than one task found with id \"" + task_id + - "\""); - } - - const auto task = task_from(result.get_item(0)); - log_debug("Retrieved task with _id " + task_id); - return task; - } catch (const exception &err) { - log_error("Failed to get task with _id " + task_id + ": " + - string(err.what())); - throw runtime_error("unable to retrieve task: " + string(err.what())); - } - } - - Task find_matching_task(const string &task_id_substring) { - try { - lock_guard lock(*mtx); - - if (task_id_substring.empty()) { - throw invalid_argument("id_substring must not be empty"); - } - const auto query = "SELECT * FROM tasks" - " WHERE contains(_id, :idSubstring)" - " AND NOT deleted"; - const auto result = ditto->get_store().execute( - query, {{"idSubstring", task_id_substring}}); - const auto item_count = result.item_count(); - if (item_count == 0) { - throw runtime_error(string("no tasks found with id containing \"") + - task_id_substring + "\""); - } else if (item_count > 1) { - throw runtime_error("more than one task found with id containing \"" + - task_id_substring + "\""); - } - - auto task = task_from(result.get_item(0)); - log_debug("Found matching task for " + task_id_substring + ": " + - task._id); - return task; - } catch (const exception &err) { - log_error("Failed to find matching task: " + string(err.what())); - throw runtime_error("unable to find matching task: " + - string(err.what())); - } - } - - void update_task(const Task &task) { - try { - lock_guard lock(*mtx); - - const auto stmt = "UPDATE tasks SET" - " title = :title," - " done = :done," - " deleted = :deleted" - " WHERE _id = :id"; - const auto result = - ditto->get_store().execute(stmt, {{"title", task.title}, - {"done", task.done}, - {"deleted", task.deleted}, - {"id", task._id}}); - if (result.mutated_document_ids().empty()) { - throw runtime_error("task not found with ID: " + task._id); - } - log_debug("Updated task: " + task._id); - } catch (const exception &err) { - log_error("Failed to update task: " + string(err.what())); - throw runtime_error("unable to update task: " + string(err.what())); - } - } - - void mark_task_complete(const string &task_id, bool done) { - try { - lock_guard lock(*mtx); - - if (task_id.empty()) { - throw invalid_argument("task ID must not be empty"); - } - - const auto stmt = "UPDATE tasks SET done = :done WHERE _id = :id"; - const auto result = - ditto->get_store().execute(stmt, {{"done", done}, {"id", task_id}}); - log_debug("Marked task " + task_id + - (done ? " complete" : " incomplete")); - } catch (const exception &err) { - log_error("Failed to mark task complete: " + string(err.what())); - throw runtime_error("unable to mark task complete: " + - string(err.what())); - } - } - - void update_task_title(const string &task_id, const string &title) { - try { - lock_guard lock(*mtx); - - if (task_id.empty()) { - throw invalid_argument("task ID must not be empty"); - } - - const auto stmt = "UPDATE tasks SET title = :title WHERE _id = :id"; - const auto result = - ditto->get_store().execute(stmt, {{"title", title}, {"id", task_id}}); - if (result.mutated_document_ids().empty()) { - throw runtime_error("task not found with ID: " + task_id); - } - log_debug("Updated task title: " + task_id); - } catch (const exception &err) { - log_error("Failed to update task title: " + string(err.what())); - throw runtime_error("unable to update task title: " + string(err.what())); - } - } - - void delete_task(const string &task_id) { - try { - lock_guard lock(*mtx); - - if (task_id.empty()) { - throw invalid_argument("task ID must not be empty"); - } - - const auto stmt = "UPDATE tasks SET deleted = true WHERE _id = :id"; - const auto result = ditto->get_store().execute(stmt, {{"id", task_id}}); - if (result.mutated_document_ids().empty()) { - throw runtime_error("task not found with ID: " + task_id); - } - log_debug("Deleted task: " + task_id); - } catch (const exception &err) { - log_error("Failed to delete task: " + string(err.what())); - throw runtime_error("unable to evict task: " + string(err.what())); - } - } - - void evict_deleted_tasks() { - try { - lock_guard lock(*mtx); - - const auto stmt = "EVICT FROM tasks WHERE deleted = true"; - ditto->get_store().execute(stmt); - log_debug("Evicted deleted tasks"); - } catch (const exception &err) { - log_error("Failed to evict deleted tasks: " + string(err.what())); - throw runtime_error("unable to evict task: " + string(err.what())); - } - } - - shared_ptr register_tasks_observer( - std::function &)> callback) { - try { - const auto observer = ditto->get_store().register_observer( - select_tasks_query(), - [callback = std::move(callback)](const ditto::QueryResult &result) { - const auto item_count = result.item_count(); - log_debug("Tasks collection updated; count=" + - to_string(item_count)); - const auto tasks = tasks_from(result); - try { - log_debug("Invoking observer callback"); - callback(tasks); - log_debug("Observer callback completed"); - } catch (const exception &err) { - log_error("Error in observer callback: " + string(err.what())); - } - }); - - log_debug("Registered tasks observer"); - return observer; - } catch (const exception &err) { - log_error("Failed to register observer: " + string(err.what())); - throw runtime_error("unable to register observer: " + string(err.what())); - } - } - - string execute_dql_query(const string &query) { - try { - lock_guard lock(*mtx); - - const auto result = ditto->get_store().execute(query); - log_debug("Executed DQL query"); - return to_json_string(result); - } catch (const exception &err) { - log_error("Failed to execute DQL query: " + string(err.what())); - throw runtime_error("unable to execute DQL query: " + string(err.what())); - } - } - - void insert_initial_tasks() { - try { - lock_guard lock(*mtx); - - std::vector initial_tasks = { - {"50191411-4C46-4940-8B72-5F8017A04FA7", "Buy groceries"}, - {"6DA283DA-8CFE-4526-A6FA-D385089364E5", "Clean the kitchen"}, - {"5303DDF8-0E72-4FEB-9E82-4B007E5797F0", - "Schedule dentist appointment"}, - {"38411F1B-6B49-4346-90C3-0B16CE97E174", "Pay bills"}}; - - for (const auto &task : initial_tasks) { - const json task_args = {{"_id", task._id}, - {"title", task.title}, - {"done", task.done}, - {"deleted", task.deleted}}; - const auto command = "INSERT INTO tasks INITIAL DOCUMENTS (:newTask)"; - ditto->get_store().execute(command, {{"newTask", task_args}}); - } - } catch (const exception &err) { - log_error("Failed to insert initial tasks: " + string(err.what())); - throw runtime_error("unable to insert initial tasks: " + - string(err.what())); - } - } -}; // class TasksPeer::Impl - -TasksPeer::TasksPeer(string app_id, string online_playground_token, - string websocket_url, string auth_url, - bool enable_cloud_sync, string persistence_dir) - : impl(new Impl(std::move(app_id), std::move(online_playground_token), - std::move(websocket_url), std::move(auth_url), - enable_cloud_sync, std::move(persistence_dir))) {} - -TasksPeer::~TasksPeer() noexcept { - try { - log_debug("Destroying TasksPeer instance"); - } catch (const std::exception &) { // NOLINT(bugprone-empty-catch) - } -} - -void TasksPeer::start_sync() { impl->start_sync(); } - -void TasksPeer::stop_sync() { impl->stop_sync(); } - -bool TasksPeer::is_sync_active() const { return impl->is_sync_active(); } - -string TasksPeer::add_task(const string &title, bool done) { - return impl->add_task(title, done); -} - -vector TasksPeer::get_tasks(bool include_deleted_tasks) { - return impl->get_tasks(include_deleted_tasks); -} - -Task TasksPeer::get_task(const string &task_id) { - return impl->get_task(task_id); -} - -Task TasksPeer::find_matching_task(const string &task_id_substring) { - return impl->find_matching_task(task_id_substring); -} - -void TasksPeer::update_task(const Task &task) { impl->update_task(task); } - -void TasksPeer::mark_task_complete(const string &task_id, bool done) { - impl->mark_task_complete(task_id, done); -} - -void TasksPeer::update_task_title(const string &task_id, const string &title) { - impl->update_task_title(task_id, title); -} - -void TasksPeer::delete_task(const string &task_id) { - impl->delete_task(task_id); -} - -void TasksPeer::evict_deleted_tasks() { impl->evict_deleted_tasks(); } - -shared_ptr TasksPeer::register_tasks_observer( - function &)> callback) { - return impl->register_tasks_observer(callback); -} - -string TasksPeer::execute_dql_query(const string &query) { - return impl->execute_dql_query(query); -} - -string TasksPeer::get_ditto_sdk_version() { - return ditto::Ditto::get_sdk_version(); -} - -void TasksPeer::insert_initial_tasks() { impl->insert_initial_tasks(); } diff --git a/cpp-tui/taskscpp/src/tasks_peer.h b/cpp-tui/taskscpp/src/tasks_peer.h deleted file mode 100644 index 3a76307cf..000000000 --- a/cpp-tui/taskscpp/src/tasks_peer.h +++ /dev/null @@ -1,116 +0,0 @@ -#ifndef DITTO_QUICKSTART_TASKS_PEER_H -#define DITTO_QUICKSTART_TASKS_PEER_H - -#include -#include -#include -#include - -#include "task.h" - -/// An agent that can create, read, update, and delete tasks, and sync them with -/// other devices. -class TasksPeer { -public: - /// Returns a string identifying the version of the Ditto SDK. - static std::string get_ditto_sdk_version(); - - /// Construct a new TasksPeer object. - TasksPeer(std::string ditto_app_id, std::string ditto_online_playground_token, - std::string ditto_websocket_url, std::string ditto_auth_url, - bool enable_cloud_sync, std::string ditto_persistence_dir); - - virtual ~TasksPeer() noexcept; - - TasksPeer(const TasksPeer &) = default; - TasksPeer(TasksPeer &&) = default; - - TasksPeer &operator=(const TasksPeer &) = delete; - TasksPeer &operator=(TasksPeer &&) = delete; - - /// Start the peer, enabling it to sync tasks with other devices. - void start_sync(); - - /// Stop the peer, disabling it from syncing tasks with other devices. - void stop_sync(); - - /// Return true if peer is currently syncing tasks with other devices. - bool is_sync_active() const; - - /// Create a new task and add it to the collection. - /// - /// @return the _id of the new task. - std::string add_task(const std::string &title, bool done); - - /// Get all tasks in the collection, optionally including those that have been - /// deleted but are still in the local store. - /// - /// This method will return a maximum of 1000 tasks. If there are more tasks - /// than that in the collection, some will be ignored. - /// - /// @return all tasks in the collection, ordered by ID. - std::vector get_tasks(bool include_deleted_tasks = false); - - /// Find a task by its ID. - /// - /// @return the Task that exactly matches the specified ID - /// - /// @throws TaskException if task cannot be retrieved. - Task get_task(const std::string &task_id); - - /// Save task properties. - void update_task(const Task &task); - - /// Find a task by a substring of its ID. - /// - /// This function is provided for use by command-line interfaces or other - /// kinds of apps where entering a full task ID is inconvenient. - /// - /// @return the Task that matches the specified ID - /// - /// @throws TaskException if task cannot be found, or if there are multiple - /// matches. - Task find_matching_task(const std::string &task_id_substring); - - /// Mark task as completed or not completed. - void mark_task_complete(const std::string &task_id, bool done); - - /// Change the title of the specified task - void update_task_title(const std::string &task_id, const std::string &title); - - /// Delete the specified task from the collection. - /// - /// Note that this marks the task as deleted, and it will no longer appear in - /// `get_tasks()` results, but the object remains in the local store until - /// @ref `evict_deleted_tasks()` is called. - void delete_task(const std::string &task_id); - - /// Remove all deleted tasks from the local store. - void evict_deleted_tasks(); - - /// Run a DQL query using the peer's Ditto instance. - /// - /// This function is provided for diagnostic purposes. It should not be used - /// for general application functionality. - /// - /// @param query DQL query string - /// @return JSON result dictionary with `items` and `modified_document_ids` - /// arrays. - std::string execute_dql_query(const std::string &query); - - /// Subscribe to updates to the tasks collection. - /// - /// @returns a subscriber object that, when destroyed, will cancel the - /// subscription. - std::shared_ptr register_tasks_observer( - std::function &)> callback); - - /// Add a set of initial documents to the tasks collection. - void insert_initial_tasks(); - -private: - class Impl; // private implementation class ("pimpl pattern") - std::shared_ptr impl; -}; - -#endif // DITTO_QUICKSTART_TASKS_PEER_H diff --git a/cpp-tui/taskscpp/src/tasks_repository.cpp b/cpp-tui/taskscpp/src/tasks_repository.cpp new file mode 100644 index 000000000..23beab050 --- /dev/null +++ b/cpp-tui/taskscpp/src/tasks_repository.cpp @@ -0,0 +1,254 @@ +#include "tasks_repository.h" +#include "tasks_log.h" + +#include "Ditto.h" + +#include +#include +#include +#include + +using namespace std; +using json = nlohmann::json; + +/// Extract a Task object from a QueryResultItem. +static Task task_from(const ditto::QueryResultItem &item) { + return json::parse(item.json_string()).template get(); +} + +/// Convert a QueryResult to a collection of Task objects. +static vector tasks_from(const ditto::QueryResult &result) { + const auto item_count = result.item_count(); + vector tasks; + tasks.reserve(item_count); + for (size_t i = 0; i < item_count; ++i) { + tasks.emplace_back(task_from(result.get_item(i))); + } + return tasks; +} + +// Private implementation of the TasksRepository class. +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) +class TasksRepository::Impl { +private: + shared_ptr mtx; + // Shared ownership of the manager keeps the Ditto instance alive for the + // repository's whole lifetime. `ditto` is cached from `manager->get_ditto()`; + // `manager` must be declared before `ditto` because `ditto` is initialized + // from it. + shared_ptr manager; + shared_ptr ditto; + shared_ptr tasks_subscription; + + string select_tasks_query() { + return "SELECT * FROM tasks WHERE NOT deleted"; + } + +public: + Impl(shared_ptr ditto_manager) + : mtx(make_shared()), manager(std::move(ditto_manager)), + ditto(manager->get_ditto()) { + // Register a subscription, which determines what data syncs to this peer. + tasks_subscription = + ditto->get_sync().register_subscription("SELECT * FROM tasks"); + } + + ~Impl() noexcept { + try { + if (tasks_subscription) { + tasks_subscription->cancel(); + tasks_subscription.reset(); + } + } catch (const exception &err) { + std::cerr << "Failed to destroy tasks repository instance: " + + string(err.what()) + << std::endl; + } + } + + string add_task(const string &title, bool done) { + try { + const json task_args = { + {"title", title}, {"done", done}, {"deleted", false}}; + const auto command = "INSERT INTO tasks DOCUMENTS (:task)"; + const auto result = + ditto->get_store().execute(command, {{"task", task_args}}); + auto task_id = result.mutated_document_ids()[0]; + log_debug("Added task: " + task_id.to_string()); + return task_id.to_string(); + } catch (const exception &err) { + log_error("Failed to add task: " + string(err.what())); + throw runtime_error("unable to add task: " + string(err.what())); + } + } + + vector get_tasks() { + try { + const auto result = ditto->get_store().execute(select_tasks_query()); + const auto tasks = tasks_from(result); + log_debug("Retrieved tasks; count=" + to_string(tasks.size())); + return tasks; + } catch (const exception &err) { + log_error("Failed to get tasks: " + string(err.what())); + throw runtime_error("unable to get tasks: " + string(err.what())); + } + } + + Task find_matching_task(const string &task_id_substring) { + try { + lock_guard lock(*mtx); + + if (task_id_substring.empty()) { + throw invalid_argument("id_substring must not be empty"); + } + const auto query = "SELECT * FROM tasks" + " WHERE contains(_id, :idSubstring)" + " AND NOT deleted"; + const auto result = ditto->get_store().execute( + query, {{"idSubstring", task_id_substring}}); + const auto item_count = result.item_count(); + if (item_count == 0) { + throw runtime_error(string("no tasks found with id containing \"") + + task_id_substring + "\""); + } else if (item_count > 1) { + throw runtime_error("more than one task found with id containing \"" + + task_id_substring + "\""); + } + + auto task = task_from(result.get_item(0)); + log_debug("Found matching task for " + task_id_substring + ": " + + task._id); + return task; + } catch (const exception &err) { + log_error("Failed to find matching task: " + string(err.what())); + throw runtime_error("unable to find matching task: " + + string(err.what())); + } + } + + void mark_task_complete(const string &task_id, bool done) { + try { + lock_guard lock(*mtx); + + if (task_id.empty()) { + throw invalid_argument("task ID must not be empty"); + } + + const auto stmt = "UPDATE tasks SET done = :done WHERE _id = :id"; + const auto result = + ditto->get_store().execute(stmt, {{"done", done}, {"id", task_id}}); + log_debug("Marked task " + task_id + + (done ? " complete" : " incomplete")); + } catch (const exception &err) { + log_error("Failed to mark task complete: " + string(err.what())); + throw runtime_error("unable to mark task complete: " + + string(err.what())); + } + } + + void update_task_title(const string &task_id, const string &title) { + try { + lock_guard lock(*mtx); + + if (task_id.empty()) { + throw invalid_argument("task ID must not be empty"); + } + + const auto stmt = "UPDATE tasks SET title = :title WHERE _id = :id"; + const auto result = + ditto->get_store().execute(stmt, {{"title", title}, {"id", task_id}}); + if (result.mutated_document_ids().empty()) { + throw runtime_error("task not found with ID: " + task_id); + } + log_debug("Updated task title: " + task_id); + } catch (const exception &err) { + log_error("Failed to update task title: " + string(err.what())); + throw runtime_error("unable to update task title: " + string(err.what())); + } + } + + void delete_task(const string &task_id) { + try { + lock_guard lock(*mtx); + + if (task_id.empty()) { + throw invalid_argument("task ID must not be empty"); + } + + const auto stmt = "UPDATE tasks SET deleted = true WHERE _id = :id"; + const auto result = ditto->get_store().execute(stmt, {{"id", task_id}}); + if (result.mutated_document_ids().empty()) { + throw runtime_error("task not found with ID: " + task_id); + } + log_debug("Deleted task: " + task_id); + } catch (const exception &err) { + log_error("Failed to delete task: " + string(err.what())); + throw runtime_error("unable to delete task: " + string(err.what())); + } + } + + shared_ptr register_tasks_observer( + std::function &)> callback) { + try { + const auto observer = ditto->get_store().register_observer( + select_tasks_query(), [callback](const ditto::QueryResult &result) { + const auto item_count = result.item_count(); + log_debug("Tasks collection updated; count=" + + to_string(item_count)); + const auto tasks = tasks_from(result); + try { + log_debug("Invoking observer callback"); + callback(tasks); + log_debug("Observer callback completed"); + } catch (const exception &err) { + log_error("Error in observer callback: " + string(err.what())); + } + }); + + log_debug("Registered tasks observer"); + return observer; + } catch (const exception &err) { + log_error("Failed to register observer: " + string(err.what())); + throw runtime_error("unable to register observer: " + string(err.what())); + } + } + +}; // class TasksRepository::Impl + +TasksRepository::TasksRepository(shared_ptr manager) + : impl(make_shared(std::move(manager))) {} + +TasksRepository::~TasksRepository() noexcept { + try { + log_debug("Destroying TasksRepository instance"); + } catch (const std::exception &) { // NOLINT(bugprone-empty-catch) + } +} + +string TasksRepository::add_task(const string &title, bool done) { + return impl->add_task(title, done); +} + +vector TasksRepository::get_tasks() { return impl->get_tasks(); } + +Task TasksRepository::find_matching_task(const string &task_id_substring) { + return impl->find_matching_task(task_id_substring); +} + +void TasksRepository::mark_task_complete(const string &task_id, bool done) { + impl->mark_task_complete(task_id, done); +} + +void TasksRepository::update_task_title(const string &task_id, + const string &title) { + impl->update_task_title(task_id, title); +} + +void TasksRepository::delete_task(const string &task_id) { + impl->delete_task(task_id); +} + +shared_ptr TasksRepository::register_tasks_observer( + function &)> callback) { + return impl->register_tasks_observer(callback); +} diff --git a/cpp-tui/taskscpp/src/tasks_repository.h b/cpp-tui/taskscpp/src/tasks_repository.h new file mode 100644 index 000000000..a7d94df95 --- /dev/null +++ b/cpp-tui/taskscpp/src/tasks_repository.h @@ -0,0 +1,85 @@ +#ifndef DITTO_QUICKSTART_TASKS_REPOSITORY_H +#define DITTO_QUICKSTART_TASKS_REPOSITORY_H + +#include +#include +#include +#include + +#include "ditto_manager.h" +#include "task.h" + +/// Owns everything specific to the tasks data: the sync subscription, the +/// task CRUD operations, and registration of the store observer that streams +/// the current task list. It talks to Ditto directly through the instance +/// vended by DittoManager. +class TasksRepository { +public: + /// Construct a repository backed by the manager's Ditto instance. This + /// registers the sync subscription so Ditto syncs matching documents from + /// other devices. + /// + /// The repository shares ownership of the manager via `std::shared_ptr`, so + /// the manager cannot be destroyed while the repository is still using it. + explicit TasksRepository(std::shared_ptr manager); + + virtual ~TasksRepository() noexcept; + + // The repository holds shared state (the sync subscription and the shared + // Ditto instance), so it is non-copyable and non-movable. Share it by + // reference or via `std::shared_ptr` rather than copying. + TasksRepository(const TasksRepository &) = delete; + TasksRepository(TasksRepository &&) = delete; + + TasksRepository &operator=(const TasksRepository &) = delete; + TasksRepository &operator=(TasksRepository &&) = delete; + + /// Create a new task and add it to the collection. + /// + /// @return the _id of the new task. + std::string add_task(const std::string &title, bool done); + + /// Get all tasks in the collection. + /// + /// This method will return a maximum of 1000 tasks. If there are more tasks + /// than that in the collection, some will be ignored. + /// + /// @return all tasks in the collection, ordered by ID. + std::vector get_tasks(); + + /// Find a task by a substring of its ID. + /// + /// This function is provided for use by command-line interfaces or other + /// kinds of apps where entering a full task ID is inconvenient. + /// + /// @return the Task that matches the specified ID + /// + /// @throws TaskException if task cannot be found, or if there are multiple + /// matches. + Task find_matching_task(const std::string &task_id_substring); + + /// Mark task as completed or not completed. + void mark_task_complete(const std::string &task_id, bool done); + + /// Change the title of the specified task + void update_task_title(const std::string &task_id, const std::string &title); + + /// Delete the specified task from the collection. + /// + /// Note that this marks the task as deleted, so it will no longer appear in + /// `get_tasks()` results; the object remains in the local store. + void delete_task(const std::string &task_id); + + /// Subscribe to updates to the tasks collection. + /// + /// @returns a subscriber object that, when destroyed, will cancel the + /// subscription. + std::shared_ptr register_tasks_observer( + std::function &)> callback); + +private: + class Impl; // private implementation class ("pimpl pattern") + std::shared_ptr impl; +}; + +#endif // DITTO_QUICKSTART_TASKS_REPOSITORY_H diff --git a/cpp-tui/taskscpp/src/tasks_tui.cpp b/cpp-tui/taskscpp/src/tasks_tui.cpp index 753d03ec8..e075a3e42 100644 --- a/cpp-tui/taskscpp/src/tasks_tui.cpp +++ b/cpp-tui/taskscpp/src/tasks_tui.cpp @@ -16,7 +16,10 @@ class TasksTui::Impl { private: - TasksPeer &peer; + TasksRepository &repository; + // Shared ownership of the manager (the same instance the repository holds) + // so sync control never outlives or diverges from the manager in use. + std::shared_ptr manager; std::vector tasks; ftxui::Component tasks_list; ftxui::ScreenInteractive screen; @@ -64,16 +67,16 @@ class TasksTui::Impl { ftxui::Component active_checkbox; for (auto &task : tasks) { - auto checkbox = - ftxui::Checkbox(task.title, &task.done, - ftxui::CheckboxOption{.on_change = [this, &task] { - try { - peer.mark_task_complete(task._id, task.done); - } catch (const std::exception &err) { - log_error("Failed to mark task complete: " + - std::string(err.what())); - } - }}); + auto checkbox = ftxui::Checkbox( + task.title, &task.done, + ftxui::CheckboxOption{.on_change = [this, &task] { + try { + repository.mark_task_complete(task._id, task.done); + } catch (const std::exception &err) { + log_error("Failed to mark task complete: " + + std::string(err.what())); + } + }}); tasks_list->Add(checkbox); if (task._id == active_task_id()) { active_checkbox = checkbox; @@ -90,10 +93,10 @@ class TasksTui::Impl { // Toggle sync on/off void toggle_sync() { try { - if (peer.is_sync_active()) { - peer.stop_sync(); + if (manager->is_sync_active()) { + manager->stop_sync(); } else { - peer.start_sync(); + manager->start_sync(); } } catch (const std::exception &err) { log_error("Failed to toggle sync: " + std::string(err.what())); @@ -113,13 +116,13 @@ class TasksTui::Impl { auto top_bar = Renderer([this] { return vbox({ hbox({text("Ditto Tasks") | bold | flex, - (peer.is_sync_active() + (manager->is_sync_active() ? text("🟢 Sync Active") | color(Color::Green) : text("šŸ”“ Sync Inactive") | color(Color::Red)) | bold, text(" (s: toggle sync)")}), - text("App ID: " DITTO_APP_ID) | center, - text("Playground Token: " DITTO_PLAYGROUND_TOKEN) | center, + text("Database ID: " DITTO_DATABASE_ID) | center, + text("Development Token: " DITTO_DEVELOPMENT_TOKEN) | center, }); }); auto bottom_bar = Renderer([this] { @@ -170,7 +173,7 @@ class TasksTui::Impl { auto task_id = active_task_id(); if (!task_id.empty()) { try { - peer.delete_task(task_id); + repository.delete_task(task_id); } catch (const std::exception &err) { log_error("Failed to delete task: " + std::string(err.what())); } @@ -202,7 +205,7 @@ class TasksTui::Impl { show_modal = false; mode = Mode::Normal; try { - peer.add_task(modal_text, false); + repository.add_task(modal_text, false); } catch (const std::exception &err) { log_error("Failed to add task: " + std::string(err.what())); } @@ -221,7 +224,7 @@ class TasksTui::Impl { show_modal = false; mode = Mode::Normal; try { - peer.update_task_title(modal_task_id, modal_text); + repository.update_task_title(modal_task_id, modal_text); } catch (const std::exception &err) { log_error("Failed to update task title: " + std::string(err.what())); @@ -239,8 +242,9 @@ class TasksTui::Impl { } public: - Impl(TasksPeer &p) - : peer(p), tasks_list(ftxui::Container::Vertical({})), + Impl(TasksRepository &r, std::shared_ptr ditto_manager) + : repository(r), manager(std::move(ditto_manager)), + tasks_list(ftxui::Container::Vertical({})), screen(ftxui::ScreenInteractive::Fullscreen()) {} ~Impl() = default; @@ -251,7 +255,7 @@ class TasksTui::Impl { std::freopen("/dev/null", "w", stderr); } - auto observer = peer.register_tasks_observer( + auto observer = repository.register_tasks_observer( [this](const std::vector &new_tasks) { screen.Post( [this, new_tasks] { update_tasks_list(std::move(new_tasks)); }); @@ -261,7 +265,9 @@ class TasksTui::Impl { } }; -TasksTui::TasksTui(TasksPeer &peer) : impl(std::make_shared(peer)) {} +TasksTui::TasksTui(TasksRepository &repository, + std::shared_ptr manager) + : impl(std::make_shared(repository, std::move(manager))) {} TasksTui::~TasksTui() {} diff --git a/cpp-tui/taskscpp/src/tasks_tui.h b/cpp-tui/taskscpp/src/tasks_tui.h index 73d950378..1258d19a7 100644 --- a/cpp-tui/taskscpp/src/tasks_tui.h +++ b/cpp-tui/taskscpp/src/tasks_tui.h @@ -3,14 +3,20 @@ #ifdef DITTO_QUICKSTART_TUI -#include "tasks_peer.h" +#include "ditto_manager.h" +#include "tasks_repository.h" #include /// Text-based interactive user interface for the Tasks application. +/// +/// The UI is handed the repository (for task data) and a shared DittoManager +/// (for sync control) directly. Both share ownership of the same manager via +/// `std::shared_ptr`, so there is a single source of truth for which manager +/// is in use. class TasksTui { public: - TasksTui(TasksPeer &peer); + TasksTui(TasksRepository &repository, std::shared_ptr manager); ~TasksTui(); diff --git a/cpp-tui/taskscpp/tests/integration_test.cpp b/cpp-tui/taskscpp/tests/integration_test.cpp index a3610d570..57575fe33 100644 --- a/cpp-tui/taskscpp/tests/integration_test.cpp +++ b/cpp-tui/taskscpp/tests/integration_test.cpp @@ -1,7 +1,8 @@ +#include "../src/ditto_manager.h" #include "../src/env.h" #include "../src/task.h" -#include "../src/tasks_peer.h" #include "../src/tasks_log.h" +#include "../src/tasks_repository.h" #include #include @@ -12,86 +13,92 @@ using std::cout; using std::endl; +using std::exception; using std::string; using std::vector; -using std::unique_ptr; -using std::exception; -using std::chrono::seconds; +using std::chrono::duration_cast; using std::chrono::high_resolution_clock; using std::chrono::milliseconds; -using std::chrono::duration_cast; +using std::chrono::seconds; using std::this_thread::sleep_for; /** * Simple test that verifies GitHub-seeded document appears in synced task list */ int main() { - try { - cout << "C++ GitHub Seeded Document Test" << endl; - - // Get the exact document title that GitHub Actions seeded - const auto expected_title_env = getenv("DITTO_CLOUD_TASK_TITLE"); - if (!expected_title_env || string(expected_title_env).empty()) { - cout << "FAIL: Missing DITTO_CLOUD_TASK_TITLE environment variable" << endl; - std::exit(EXIT_FAILURE); - } + try { + cout << "C++ GitHub Seeded Document Test" << endl; + + // Get the exact document title that GitHub Actions seeded + const auto expected_title_env = getenv("DITTO_CLOUD_TASK_TITLE"); + if (!expected_title_env || string(expected_title_env).empty()) { + cout << "FAIL: Missing DITTO_CLOUD_TASK_TITLE environment variable" + << endl; + std::exit(EXIT_FAILURE); + } - string expected_title = string(expected_title_env); - cout << "Looking for seeded document: '" << expected_title << "'" << endl; - - // Initialize TasksPeer and start sync - cout << "Initializing Ditto sync..." << endl; - auto peer = unique_ptr(new TasksPeer( - DITTO_APP_ID, - DITTO_PLAYGROUND_TOKEN, - DITTO_WEBSOCKET_URL, - DITTO_AUTH_URL, - true, // enable_cloud_sync - "/tmp/cpp_integration_test" - )); - - peer->start_sync(); - cout << "Sync started, polling for document..." << endl; - - // Wait for sync and search for the exact document - const auto max_wait_seconds = 30; - const auto poll_interval_ms = 1000; - auto found = false; - - auto start_time = high_resolution_clock::now(); - - while (duration_cast(high_resolution_clock::now() - start_time).count() < max_wait_seconds && !found) { - auto elapsed = duration_cast(high_resolution_clock::now() - start_time).count(); - - vector tasks = peer->get_tasks(); - cout << "Checking " << tasks.size() << " synced tasks at " << elapsed << "s..." << endl; - - for (size_t i = 0; i < tasks.size(); i++) { - const auto& task = tasks[i]; - if (task.title == expected_title) { - cout << "SUCCESS: Found document '" << expected_title << "' at position " << i << endl; - found = true; - break; - } - } - - if (!found) { - sleep_for(milliseconds(poll_interval_ms)); - } + string expected_title = string(expected_title_env); + cout << "Looking for seeded document: '" << expected_title << "'" << endl; + + // Initialize the Ditto manager and tasks repository, then start sync + cout << "Initializing Ditto sync..." << endl; + auto manager = std::make_shared( + DITTO_DATABASE_ID, DITTO_DEVELOPMENT_TOKEN, DITTO_SERVER_URL, + "/tmp/cpp_integration_test"); + TasksRepository repository(manager); + + manager->start_sync(); + cout << "Sync started, polling for document..." << endl; + + // Wait for sync and search for the exact document + const auto max_wait_seconds = 30; + const auto poll_interval_ms = 1000; + auto found = false; + + auto start_time = high_resolution_clock::now(); + + while (duration_cast(high_resolution_clock::now() - start_time) + .count() < max_wait_seconds && + !found) { + auto elapsed = + duration_cast(high_resolution_clock::now() - start_time) + .count(); + + vector tasks = repository.get_tasks(); + cout << "Checking " << tasks.size() << " synced tasks at " << elapsed + << "s..." << endl; + + for (size_t i = 0; i < tasks.size(); i++) { + const auto &task = tasks[i]; + if (task.title == expected_title) { + cout << "SUCCESS: Found document '" << expected_title + << "' at position " << i << endl; + found = true; + break; } + } - auto final_elapsed = duration_cast(high_resolution_clock::now() - start_time).count(); - - if (found) { - cout << "PASS: GitHub Actions → Ditto Cloud → C++ SDK sync verified in " << final_elapsed << "s" << endl; - return 0; - } else { - cout << "FAIL: Document '" << expected_title << "' not found after " << final_elapsed << "s" << endl; - std::exit(EXIT_FAILURE); - } + if (!found) { + sleep_for(milliseconds(poll_interval_ms)); + } + } - } catch (const exception& e) { - cout << "FAIL: Test exception: " << e.what() << endl; - std::exit(EXIT_FAILURE); + auto final_elapsed = + duration_cast(high_resolution_clock::now() - start_time) + .count(); + + if (found) { + cout << "PASS: GitHub Actions → Ditto server → C++ SDK sync verified in " + << final_elapsed << "s" << endl; + return 0; + } else { + cout << "FAIL: Document '" << expected_title << "' not found after " + << final_elapsed << "s" << endl; + std::exit(EXIT_FAILURE); } + + } catch (const exception &e) { + cout << "FAIL: Test exception: " << e.what() << endl; + std::exit(EXIT_FAILURE); + } } \ No newline at end of file diff --git a/cpp-tui/taskscpp/tests/run_tests.sh b/cpp-tui/taskscpp/tests/run_tests.sh index 201251448..9a3a7923c 100755 --- a/cpp-tui/taskscpp/tests/run_tests.sh +++ b/cpp-tui/taskscpp/tests/run_tests.sh @@ -36,7 +36,7 @@ echo "=================================" g++ -std=c++17 \ -I./src -I./sdk -I./third_party/cxxopts/include \ tests/integration_test.cpp \ - src/task.cpp src/tasks_peer.cpp src/tasks_log.cpp \ + src/task.cpp src/ditto_manager.cpp src/tasks_repository.cpp src/tasks_log.cpp \ -L./sdk -lditto \ -o build/tests/integration_test \ -pthread @@ -59,4 +59,4 @@ echo "" echo "šŸŽ‰ Integration Tests Completed Successfully!" echo "==========================================" echo "āœ… Integration tests: PASSED" -echo "šŸŽÆ C++ Ditto TUI application validated!" \ No newline at end of file +echo "šŸŽÆ C++ Ditto TUI application validated!" diff --git a/dotnet-maui/DittoMauiTasksApp/DittoManager.cs b/dotnet-maui/DittoMauiTasksApp/DittoManager.cs new file mode 100644 index 000000000..4a61629fc --- /dev/null +++ b/dotnet-maui/DittoMauiTasksApp/DittoManager.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +using DittoSDK; +using DittoSDK.Auth; + +namespace DittoMauiTasksApp +{ + ///

+ /// Manages the lifecycle and configuration of a Ditto instance: config, open, + /// auth, and starting/stopping sync. It vends a configured, already-running + /// instance (via the property) for the + /// tasks repository to use directly. + /// + /// + /// This class deliberately does NOT wrap Ditto's APIs (store/sync/etc.). The + /// repository calls the real Ditto API — e.g. dittoManager.Ditto.Store.ExecuteAsync(...) + /// — so the quickstart shows how to use Ditto directly rather than modeling an + /// abstraction layer over it. + /// + public class DittoManager : IDisposable + { + public string DatabaseId { get; private set; } + public string DevelopmentToken { get; private set; } + public string ServerUrl { get; private set; } + + public bool IsSyncActive => _ditto.Sync.IsActive; + + private readonly Ditto _ditto; + + /// + /// The configured Ditto instance. The tasks repository uses this to call + /// the real Ditto API directly. + /// + public Ditto Ditto => _ditto; + + /// + /// Creates a new DittoManager instance with authentication configured. + /// + /// + /// Unlike the desktop .NET quickstarts, sync is NOT started here: this runs + /// at app-launch time (before any page/activity exists), and MAUI must + /// request runtime sync permissions from a UI context before starting the + /// transports. The ViewModel requests permissions and then calls + /// . + /// + public static Task Create( + string databaseId, + string developmentToken, + string serverUrl) + { + var dittoManager = new DittoManager(databaseId, developmentToken, serverUrl); + dittoManager.Authenticate(); + + return Task.FromResult(dittoManager); + } + + private void Authenticate() + { + _ditto.Auth.ExpirationHandler = async (ditto, secondsRemaining) => + { + // Authenticate when token is expiring + try + { + await ditto.Auth.LoginAsync( + // Your development token, replace with your actual token + DevelopmentToken, + // Use DittoAuthenticationProvider.Development, or your actual provider + DittoAuthenticationProvider.Development + ); + Console.WriteLine("Authentication successful"); + } + catch (Exception error) + { + Console.WriteLine($"Authentication failed: {error}"); + } + }; + } + + /// + /// Constructor + /// + /// Ditto database ID + /// Ditto development token + /// Ditto Server URL + private DittoManager(string databaseId, string developmentToken, string serverUrl) + { + DatabaseId = databaseId; + DevelopmentToken = developmentToken; + ServerUrl = serverUrl; + + // New Initialization code - https://docs.ditto.live/sdk/latest/ditto-config + var config = new DittoConfig( + DatabaseId, + new DittoConfigConnect.Server(new Uri(serverUrl)), + Path.Combine(FileSystem.Current.AppDataDirectory, "ditto") + ); + + _ditto = Ditto.Open(config); + } + + public void Dispose() + { + _ditto.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// Start synchronizing with other peers and the Ditto server. + /// + public void StartSync() + { + _ditto.Sync.Start(); + } + + /// + /// Stop synchronizing. + /// + /// + /// This stops the sync engine but leaves subscriptions registered, so + /// toggling sync back on resumes syncing the tasks collection. The + /// subscription's lifecycle is owned by . + /// + public void StopSync() + { + _ditto.Sync.Stop(); + } + } +} diff --git a/dotnet-maui/DittoMauiTasksApp/DittoMauiTasksApp.csproj b/dotnet-maui/DittoMauiTasksApp/DittoMauiTasksApp.csproj index 5619b566a..77a8af163 100644 --- a/dotnet-maui/DittoMauiTasksApp/DittoMauiTasksApp.csproj +++ b/dotnet-maui/DittoMauiTasksApp/DittoMauiTasksApp.csproj @@ -1,67 +1,67 @@ - - - - net10.0-android;net10.0-ios;net10.0-maccatalyst - net10.0-windows10.0.19041.0;net10.0-android;net10.0-ios - net10.0-android - Exe - DittoMauiTasksApp - true - true - enable - - - DittoMauiTasksApp - - - live.ditto.quickstart.mauitasksapp - c8b2d96a-f86c-493c-9466-4a893592fbf9 - - 1.0 - 1 - - 15.0 - 24.0 - 15.0 - 10.0.17763.0 - - - - - QS Tasks - - - - - partial - true - all - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + net10.0-android;net10.0-ios;net10.0-maccatalyst + net10.0-windows10.0.19041.0;net10.0-android;net10.0-ios + net10.0-android + Exe + DittoMauiTasksApp + true + true + enable + + + DittoMauiTasksApp + + + live.ditto.quickstart.mauitasksapp + c8b2d96a-f86c-493c-9466-4a893592fbf9 + + 1.0 + 1 + + 15.0 + 24.0 + 15.0 + 10.0.17763.0 + + + + + QS Tasks + + + + + partial + true + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet-maui/DittoMauiTasksApp/MauiProgram.cs b/dotnet-maui/DittoMauiTasksApp/MauiProgram.cs index e316382a6..41e8d4a3f 100644 --- a/dotnet-maui/DittoMauiTasksApp/MauiProgram.cs +++ b/dotnet-maui/DittoMauiTasksApp/MauiProgram.cs @@ -1,131 +1,101 @@ -using System.Reflection; - -using Microsoft.Extensions.Logging; - -using DittoMauiTasksApp.Utils; -using DittoMauiTasksApp.ViewModels; -using DittoSDK; -using DittoSDK.Auth; - -namespace DittoMauiTasksApp; - -public static class MauiProgram -{ - public static string AppId { get; private set; } = ""; - public static string PlaygroundToken { get; private set; } = ""; - - public static MauiApp CreateMauiApp() - { - var builder = MauiApp.CreateBuilder(); - builder - .UseMauiApp() - .ConfigureFonts(fonts => - { - fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); - fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); - }); - -#if DEBUG - builder.Logging.SetMinimumLevel(LogLevel.Debug); - builder.Logging.AddDebug(); -#endif - builder.Services.AddSingleton(SetupDitto()); - builder.Services.AddSingleton(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - - return builder.Build(); - } - - private static Ditto SetupDitto() - { - var envVars = LoadEnvVariables(); - AppId = envVars["DITTO_APP_ID"]; - PlaygroundToken = envVars["DITTO_PLAYGROUND_TOKEN"]; - var authUrl = envVars["DITTO_AUTH_URL"]; - - // New Initialization code - https://docs.ditto.live/sdk/latest/ditto-config - var dittoConfig = new DittoConfig( - AppId, - new DittoConfigConnect.Server( - new Uri(authUrl) - ), - Path.Combine(FileSystem.Current.AppDataDirectory, "ditto") - ); - - var ditto = Ditto.Open(dittoConfig); - - // Set up authentication expiration handler (required for server connections) - ditto.Auth.ExpirationHandler = async (dittoAuth, secondsRemaining) => - { - // Authenticate when token is expiring - try - { - await dittoAuth.Auth.LoginAsync( - // Your development token, replace with your actual token - PlaygroundToken, - // Use DittoAuthenticationProvider.Development for playground, or your actual provider - DittoAuthenticationProvider.Development - ); - Console.WriteLine("Authentication successful"); - } - catch (Exception error) - { - Console.WriteLine($"Authentication failed: {error}"); - } - }; - - return ditto; - } - - /// - /// Load environment variables from the embedded .env resource file. - /// - static Dictionary LoadEnvVariables() - { - var envVars = new Dictionary(); - var assembly = Assembly.GetExecutingAssembly(); - string resourceName = "DittoMauiTasksApp..env"; - - using (Stream stream = assembly.GetManifestResourceStream(resourceName)) - { - if (stream == null) - { - var availableResources = string.Join(Environment.NewLine, assembly.GetManifestResourceNames()); - throw new InvalidOperationException($"Resource '{resourceName}' not found. Available resources: {availableResources}"); - } - - using (var reader = new StreamReader(stream)) - { - string line; - while ((line = reader.ReadLine()) != null) - { - line = line.Trim(); - - if (string.IsNullOrEmpty(line) || line.StartsWith("#")) - { - continue; - } - - int separatorIndex = line.IndexOf('='); - if (separatorIndex < 0) - { - continue; - } - - string key = line.Substring(0, separatorIndex).Trim(); - string value = line.Substring(separatorIndex + 1).Trim(); - - if (value.StartsWith("\"") && value.EndsWith("\"") && value.Length >= 2) - { - value = value.Substring(1, value.Length - 2); - } - - envVars[key] = value; - } - } - } - - return envVars; - } -} +using System.Reflection; + +using Microsoft.Extensions.Logging; + +using DittoMauiTasksApp.Utils; +using DittoMauiTasksApp.ViewModels; + +namespace DittoMauiTasksApp; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + +#if DEBUG + builder.Logging.SetMinimumLevel(LogLevel.Debug); + builder.Logging.AddDebug(); +#endif + + var envVars = LoadEnvVariables(); + var databaseId = envVars["DITTO_DATABASE_ID"]; + var developmentToken = envVars["DITTO_DEVELOPMENT_TOKEN"]; + var serverUrl = envVars["DITTO_SERVER_URL"]; + + // Create the Ditto instance and the tasks repository up front, then + // register them as singletons. Create() is synchronous under the hood, + // so resolving the Task here does not block. + var dittoManager = DittoManager + .Create(databaseId, developmentToken, serverUrl) + .GetAwaiter() + .GetResult(); + var tasksRepository = new TasksRepository(dittoManager); + + builder.Services.AddSingleton(dittoManager); + builder.Services.AddSingleton(tasksRepository); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + return builder.Build(); + } + + /// + /// Load environment variables from the embedded .env resource file. + /// + static Dictionary LoadEnvVariables() + { + var envVars = new Dictionary(); + var assembly = Assembly.GetExecutingAssembly(); + string resourceName = "DittoMauiTasksApp..env"; + + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + var availableResources = string.Join(Environment.NewLine, assembly.GetManifestResourceNames()); + throw new InvalidOperationException($"Resource '{resourceName}' not found. Available resources: {availableResources}"); + } + + using (var reader = new StreamReader(stream)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + line = line.Trim(); + + if (string.IsNullOrEmpty(line) || line.StartsWith("#")) + { + continue; + } + + int separatorIndex = line.IndexOf('='); + if (separatorIndex < 0) + { + continue; + } + + string key = line.Substring(0, separatorIndex).Trim(); + string value = line.Substring(separatorIndex + 1).Trim(); + + if (value.StartsWith("\"") && value.EndsWith("\"") && value.Length >= 2) + { + value = value.Substring(1, value.Length - 2); + } + + envVars[key] = value; + } + } + } + + return envVars; + } +} diff --git a/dotnet-maui/DittoMauiTasksApp/Model/DittoTask.cs b/dotnet-maui/DittoMauiTasksApp/Model/DittoTask.cs deleted file mode 100644 index 328156132..000000000 --- a/dotnet-maui/DittoMauiTasksApp/Model/DittoTask.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text.Json.Serialization; -using CommunityToolkit.Mvvm.ComponentModel; -using DittoSDK; - -namespace DittoMauiTasksApp -{ - public partial class DittoTask : ObservableObject - { - [ObservableProperty] - [property: JsonPropertyName("_id")] - string id; - - [ObservableProperty] - [property: JsonPropertyName("title")] - string title; - - [ObservableProperty] - [property: JsonPropertyName("done")] - bool done; - - [ObservableProperty] - [property: JsonPropertyName("deleted")] - bool deleted; - } -} diff --git a/dotnet-maui/DittoMauiTasksApp/Model/TaskModel.cs b/dotnet-maui/DittoMauiTasksApp/Model/TaskModel.cs new file mode 100644 index 000000000..651d669e6 --- /dev/null +++ b/dotnet-maui/DittoMauiTasksApp/Model/TaskModel.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; +using DittoSDK; + +namespace DittoMauiTasksApp +{ + /// + /// Representation of a document in the Ditto 'tasks' collection. + /// + /// + /// This class is named TaskModel rather than Task to avoid + /// conflicts with the System.Threading.Tasks.Task class. It derives + /// from ObservableObject so the MAUI UI is notified of in-place + /// property changes (e.g. two-way binding on the "done" checkbox). + /// + public partial class TaskModel : ObservableObject + { + [ObservableProperty] + [property: JsonPropertyName("_id")] + string id; + + [ObservableProperty] + [property: JsonPropertyName("title")] + string title; + + [ObservableProperty] + [property: JsonPropertyName("done")] + bool done; + + [ObservableProperty] + [property: JsonPropertyName("deleted")] + bool deleted; + } +} diff --git a/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/AppDelegate.cs b/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/AppDelegate.cs index b2dfc7656..47b449a56 100644 --- a/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/AppDelegate.cs +++ b/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/AppDelegate.cs @@ -6,4 +6,4 @@ namespace DittoMauiTasksApp; public class AppDelegate : MauiUIApplicationDelegate { protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); -} \ No newline at end of file +} diff --git a/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/Program.cs b/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/Program.cs index c252a7fb9..544b0218f 100644 --- a/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/Program.cs +++ b/dotnet-maui/DittoMauiTasksApp/Platforms/MacCatalyst/Program.cs @@ -12,4 +12,4 @@ static void Main(string[] args) // you can specify it here. UIApplication.Main(args, null, typeof(AppDelegate)); } -} \ No newline at end of file +} diff --git a/dotnet-maui/DittoMauiTasksApp/TasksRepository.cs b/dotnet-maui/DittoMauiTasksApp/TasksRepository.cs new file mode 100644 index 000000000..7ab93bc70 --- /dev/null +++ b/dotnet-maui/DittoMauiTasksApp/TasksRepository.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +using DittoSDK.Store; + +namespace DittoMauiTasksApp +{ + /// + /// Owns all 'tasks' collection data operations: the DQL query strings, the sync + /// subscription, the store observer, and CRUD. It calls the real Ditto API + /// directly through the it is given. + /// + public class TasksRepository + { + // The subscription controls what syncs to this device and is written to + // the local database; the observer reacts to changes in that local + // database, hiding soft-deleted tasks from the displayed list. + private const string SubscriptionQuery = "SELECT * FROM tasks"; + private const string ObserverQuery = "SELECT * FROM tasks WHERE NOT deleted"; + + private readonly DittoManager _dittoManager; + + public TasksRepository(DittoManager dittoManager) + { + _dittoManager = dittoManager; + RegisterSubscription(); + } + + /// + /// Registers a subscription for the tasks collection to enable data synchronization + /// with other peers and the Ditto server. This subscription determines what data + /// will be synced to this device during the synchronization process. + /// + /// + /// The subscription syncs all tasks (SELECT * FROM tasks) and controls + /// what is written to the local database; the observer query hides + /// soft-deleted tasks from the displayed list. + /// + /// + private void RegisterSubscription() + { + // Register a subscription, which determines what data syncs to this device + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + _dittoManager.Ditto.Sync.RegisterSubscription(SubscriptionQuery); + } + + /// + /// Adds a new task to the 'tasks' collection. + /// + public async Task AddTask(string title) + { + if (string.IsNullOrWhiteSpace(title)) + { + throw new ArgumentException("title cannot be empty"); + } + + var doc = new Dictionary + { + {"title", title}, + {"done", false}, + {"deleted", false } + }; + const string insertCommand = "INSERT INTO tasks DOCUMENTS (:task)"; + await _dittoManager.Ditto.Store.ExecuteAsync(insertCommand, new Dictionary() + { + { "task", doc } + }); + } + + /// + /// Update the title of an existing task. + /// + public async Task UpdateTaskTitle(string taskId, string newTitle) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + if (string.IsNullOrWhiteSpace(newTitle)) + { + throw new ArgumentException("title cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET title = :title WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + {"title", newTitle}, + {"id", taskId} + }); + } + + /// + /// Mark a task as deleted. + /// + public async Task DeleteTask(string taskId) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET deleted = true WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + { "id", taskId } + }); + } + + /// + /// Mark a task as complete or not complete. + /// + public async Task UpdateTaskDone(string taskId, bool newDoneState) + { + const string updateQuery = "UPDATE tasks SET done = :done WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary + { + { "done", newDoneState }, + { "id", taskId } + }); + } + + /// + /// Specify a handler to be called asynchronously when the task collection changes. + /// + public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) + { + // Register an observer, which runs against the local database on this device + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + return _dittoManager.Ditto.Store.RegisterObserver(ObserverQuery, async (queryResult) => + { + try + { + // Deserialize the JSON documents into TaskModel objects. + var tasks = queryResult.Items.Select(d => + JsonSerializer.Deserialize(d.JsonString()) + ).ToList(); + + await handler(tasks); + } + catch (Exception e) + { + Console.Error.WriteLine($"ERROR tasks observation handler failed: {e.Message}"); + } + }); + } + } +} diff --git a/dotnet-maui/DittoMauiTasksApp/Utils/PopupService.cs b/dotnet-maui/DittoMauiTasksApp/Utils/PopupService.cs index f158f5b22..aa46f2e7a 100644 --- a/dotnet-maui/DittoMauiTasksApp/Utils/PopupService.cs +++ b/dotnet-maui/DittoMauiTasksApp/Utils/PopupService.cs @@ -1,14 +1,14 @@ -using System; - -namespace DittoMauiTasksApp.Utils -{ - public class PopupService : IPopupService - { - public Task DisplayPromptAsync(string title, string message, string placeholder, string initialValue = "") - { - Page page = Application.Current?.Windows[0].Page; - return page.DisplayPromptAsync(title, message, placeholder: placeholder, initialValue: initialValue); - } - } -} - +using System; + +namespace DittoMauiTasksApp.Utils +{ + public class PopupService : IPopupService + { + public Task DisplayPromptAsync(string title, string message, string placeholder, string initialValue = "") + { + Page page = Application.Current?.Windows[0].Page; + return page.DisplayPromptAsync(title, message, placeholder: placeholder, initialValue: initialValue); + } + } +} + diff --git a/dotnet-maui/DittoMauiTasksApp/ViewModels/TasksPageviewModel.cs b/dotnet-maui/DittoMauiTasksApp/ViewModels/TasksPageviewModel.cs index 066ff0113..9246357a0 100644 --- a/dotnet-maui/DittoMauiTasksApp/ViewModels/TasksPageviewModel.cs +++ b/dotnet-maui/DittoMauiTasksApp/ViewModels/TasksPageviewModel.cs @@ -1,370 +1,308 @@ - -using System.Collections.ObjectModel; -using System.Text.Json; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using DittoMauiTasksApp.Utils; -using DittoSDK; -using DittoSDK.Sync; -using Microsoft.Extensions.Logging; - -namespace DittoMauiTasksApp.ViewModels -{ - public partial class TasksPageviewModel : ObservableObject - { - private const string SelectQuery = "SELECT * FROM tasks WHERE NOT deleted"; - - private readonly Ditto ditto; - private readonly IPopupService popupService; - private readonly ILogger logger; - private DittoSyncSubscription syncSubscription; - - public string AppIdText { get; } = $"App ID: {MauiProgram.AppId}"; - public string TokenText { get; } = $"Token: {MauiProgram.PlaygroundToken}"; - - [ObservableProperty] - ObservableCollection tasks; - - [ObservableProperty] - private bool isSyncEnabled = true; - - public TasksPageviewModel( - Ditto ditto, IPopupService popupService, ILogger logger) - { - this.ditto = ditto; - this.popupService = popupService; - this.logger = logger; -#if WINDOWS - try - { - Task.Run(async () => - { - await InsertInitialTasks(); - ObserveDittoTasksCollection(); - StartSync(); - - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Unable to start Ditto sync: {e.Message}"); - } -#else - - DittoSyncPermissions.RequestPermissionsAsync().ContinueWith(async t => - { - try - { - await InsertInitialTasks(); - ObserveDittoTasksCollection(); - StartSync(); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Unable to start Ditto sync: {e.Message}"); - } - }); -#endif - } - private async Task InsertInitialTasks() - { - try - { - var initialTasks = new List> - { - new Dictionary - { - {"_id", "50191411-4C46-4940-8B72-5F8017A04FA7"}, - {"title", "Buy groceries"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "6DA283DA-8CFE-4526-A6FA-D385089364E5"}, - {"title", "Clean the kitchen"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "5303DDF8-0E72-4FEB-9E82-4B007E5797F0"}, - {"title", "Schedule dentist appointment"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "38411F1B-6B49-4346-90C3-0B16CE97E174"}, - {"title", "Pay bills"}, - {"done", false}, - {"deleted", false} - } - }; - - var insertCommand = "INSERT INTO tasks INITIAL DOCUMENTS (:task)"; - foreach (var task in initialTasks) - { - await ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "task", task } - }); - } - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error adding initial tasks: {e.Message}"); - } - } - - [RelayCommand] - private async Task AddTaskAsync() - { - try - { - var title = await popupService.DisplayPromptAsync( - "Add Task", "Add a new task:", "Task title"); - - if (string.IsNullOrWhiteSpace(title)) - { - // nothing was entered - return; - } - title.Trim(); - - var doc = new Dictionary - { - {"title", title}, - {"done", false}, - {"deleted", false } - }; - var insertCommand = "INSERT INTO tasks DOCUMENTS (:doc)"; - await ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "doc", doc } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error adding task: {e.Message}"); - } - } - - [RelayCommand] - private async Task EditTaskAsync(DittoTask task) - { - try - { - var newTitle = await popupService.DisplayPromptAsync( - "Edit Task", "Change task title:", "Task title", - initialValue: task.Title); - - if (string.IsNullOrWhiteSpace(newTitle)) - { - // nothing was entered - return; - } - newTitle.Trim(); - - var updateQuery = "UPDATE tasks " + - "SET title = :title " + - "WHERE _id = :id"; - await ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - {"title", newTitle}, - {"id", task.Id} - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error editing task: {e.Message}"); - } - } - - [RelayCommand] - private void DeleteTask(DittoTask task) - { - try - { - var updateQuery = "UPDATE tasks " + - "SET deleted = true " + - "WHERE _id = :id"; - ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - { "id", task.Id } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error deleting task: {e.Message}"); - } - } - - [RelayCommand] - private Task UpdateTaskDoneAsync(DittoTask task) - { - try - { - if (task == null) - { - logger.LogWarning("TasksPageviewModel: UpdateTaskDoneAsync called with null task"); - return Task.CompletedTask; - } - - var taskId = task.Id; - var newDoneState = task.Done; - - // Fire-and-forget the Ditto update to avoid blocking the UI - // thread while handling a checkbox change - _ = Task.Run(async () => - { - try - { - // Update the task done state only if it has changed, to - // avoid unnecessary calls to the store observer callback. - var updateQuery = "UPDATE tasks " + - "SET done = :newDoneState " + - "WHERE _id = :id AND done != :newDoneState"; - var result = await ditto.Store.ExecuteAsync(updateQuery, new Dictionary - { - { "newDoneState", newDoneState }, - { "id", taskId } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error updating task done state for {taskId}: {e.Message}"); - } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error updating task done state: {e.Message}"); - } - return Task.CompletedTask; - } - - private void ObserveDittoTasksCollection() - { - // Register observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - ditto.Store.RegisterObserver(SelectQuery, async (queryResult) => - { - try - { - var newTasks = queryResult.Items.Select(d => - JsonSerializer.Deserialize(d.JsonString()) - ).ToList(); - - MainThread.BeginInvokeOnMainThread(() => - { - try - { - if (Tasks == null) - { - Tasks = new ObservableCollection(newTasks); - } - else - { - UpdateTasks(newTasks); - } - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error: Unable to update list view model: {e.Message}"); - } - }); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error: Unable to process tasks collection change: {e.Message}"); - } - }); - } - - private void UpdateTasks(List newTasks) - { - var oldCount = Tasks.Count; - var newCount = newTasks.Count; - var minCount = Math.Min(oldCount, newCount); - - for (var i = 0; i < minCount; i++) - { - var existingTask = Tasks[i]; - var newTask = newTasks[i]; - existingTask.Id = newTask.Id; - existingTask.Title = newTask.Title; - existingTask.Done = newTask.Done; - existingTask.Deleted = newTask.Deleted; - } - - if (oldCount < newCount) - { - for (var i = oldCount; i < newCount; i++) - { - Tasks.Add(newTasks[i]); - } - } - else if (oldCount > newCount) - { - for (var i = oldCount - 1; i >= newCount; i--) - { - Tasks.RemoveAt(i); - } - } - } - - partial void OnIsSyncEnabledChanged(bool value) - { - if (value) - { - StartSync(); - } - else - { - StopSync(); - } - } - - private void StartSync() - { - try - { - ditto.Sync.Start(); - - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - syncSubscription = ditto.Sync.RegisterSubscription(SelectQuery); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error starting Ditto sync: {e.Message}"); - } - } - - private void StopSync() - { - if (syncSubscription != null) - { - try - { - syncSubscription.Cancel(); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error cancelling sync subscription: {e.Message}"); - } - syncSubscription = null; - } - - try - { - ditto.Sync.Stop(); - } - catch (Exception e) - { - logger.LogError($"TasksPageviewModel: Error stopping Ditto sync: {e.Message}"); - } - } - } -} + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DittoMauiTasksApp.Utils; +using DittoSDK; +using DittoSDK.Store; +using DittoSDK.Sync; +using Microsoft.Extensions.Logging; + +namespace DittoMauiTasksApp.ViewModels +{ + public partial class TasksPageviewModel : ObservableObject, IDisposable + { + private readonly DittoManager dittoManager; + private readonly TasksRepository tasksRepository; + private readonly IPopupService popupService; + private readonly ILogger logger; + private DittoStoreObserver tasksObserver; + // Registration runs on the permissions-task continuation (thread-pool thread) + // while Dispose() runs on MAUI's page teardown, so the disposed flag and the + // observer handoff are guarded by this lock to make the check-and-store atomic. + private readonly object observerGate = new(); + private bool disposed; + + public string DatabaseIdText { get; } + public string TokenText { get; } + + [ObservableProperty] + ObservableCollection tasks; + + [ObservableProperty] + private bool isSyncEnabled = true; + + public TasksPageviewModel( + DittoManager dittoManager, + TasksRepository tasksRepository, + IPopupService popupService, + ILogger logger) + { + this.dittoManager = dittoManager; + this.tasksRepository = tasksRepository; + this.popupService = popupService; + this.logger = logger; + + DatabaseIdText = $"Database ID: {dittoManager.DatabaseId}"; + TokenText = $"Token: {dittoManager.DevelopmentToken}"; + +#if WINDOWS + try + { + Task.Run(() => + { + ObserveDittoTasksCollection(); + StartSync(); + + }); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Unable to start Ditto sync: {e.Message}"); + } +#else + + DittoSyncPermissions.RequestPermissionsAsync().ContinueWith(t => + { + try + { + ObserveDittoTasksCollection(); + StartSync(); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Unable to start Ditto sync: {e.Message}"); + } + }); +#endif + } + + [RelayCommand] + private async Task AddTaskAsync() + { + try + { + var title = await popupService.DisplayPromptAsync( + "Add Task", "Add a new task:", "Task title"); + + if (string.IsNullOrWhiteSpace(title)) + { + // nothing was entered + return; + } + title.Trim(); + + await tasksRepository.AddTask(title); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error adding task: {e.Message}"); + } + } + + [RelayCommand] + private async Task EditTaskAsync(TaskModel task) + { + try + { + var newTitle = await popupService.DisplayPromptAsync( + "Edit Task", "Change task title:", "Task title", + initialValue: task.Title); + + if (string.IsNullOrWhiteSpace(newTitle)) + { + // nothing was entered + return; + } + newTitle.Trim(); + + await tasksRepository.UpdateTaskTitle(task.Id, newTitle); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error editing task: {e.Message}"); + } + } + + [RelayCommand] + private void DeleteTask(TaskModel task) + { + try + { + _ = tasksRepository.DeleteTask(task.Id); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error deleting task: {e.Message}"); + } + } + + [RelayCommand] + private Task UpdateTaskDoneAsync(TaskModel task) + { + try + { + if (task == null) + { + logger.LogWarning("TasksPageviewModel: UpdateTaskDoneAsync called with null task"); + return Task.CompletedTask; + } + + var taskId = task.Id; + var newDoneState = task.Done; + + // Fire-and-forget the Ditto update to avoid blocking the UI + // thread while handling a checkbox change + _ = Task.Run(async () => + { + try + { + await tasksRepository.UpdateTaskDone(taskId, newDoneState); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error updating task done state for {taskId}: {e.Message}"); + } + }); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error updating task done state: {e.Message}"); + } + return Task.CompletedTask; + } + + private void ObserveDittoTasksCollection() + { + // Register observer, which runs against the local database on this peer + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + var observer = tasksRepository.ObserveTasksCollection(newTasks => + { + MainThread.BeginInvokeOnMainThread(() => + { + try + { + if (Tasks == null) + { + Tasks = new ObservableCollection(newTasks); + } + else + { + UpdateTasks(newTasks); + } + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error: Unable to update list view model: {e.Message}"); + } + }); + + return Task.CompletedTask; + }); + + // Hand the observer off (or, if Dispose() already ran, cancel the just-created + // one so it isn't leaked) atomically with respect to Dispose(). + lock (observerGate) + { + if (!disposed) + { + tasksObserver = observer; + return; + } + } + + observer.Cancel(); + observer.Dispose(); + } + + public void Dispose() + { + DittoStoreObserver observer; + lock (observerGate) + { + if (disposed) + { + return; + } + disposed = true; + observer = tasksObserver; + tasksObserver = null; + } + + // Cancel and dispose the store observer so its Ditto callback is not leaked when + // this view model goes away. The sync subscription is owned by the singleton + // TasksRepository (registered in MauiProgram), not this view model, so it is + // intentionally left alone here. + observer?.Cancel(); + observer?.Dispose(); + } + + private void UpdateTasks(IList newTasks) + { + var oldCount = Tasks.Count; + var newCount = newTasks.Count; + var minCount = Math.Min(oldCount, newCount); + + for (var i = 0; i < minCount; i++) + { + var existingTask = Tasks[i]; + var newTask = newTasks[i]; + existingTask.Id = newTask.Id; + existingTask.Title = newTask.Title; + existingTask.Done = newTask.Done; + existingTask.Deleted = newTask.Deleted; + } + + if (oldCount < newCount) + { + for (var i = oldCount; i < newCount; i++) + { + Tasks.Add(newTasks[i]); + } + } + else if (oldCount > newCount) + { + for (var i = oldCount - 1; i >= newCount; i--) + { + Tasks.RemoveAt(i); + } + } + } + + partial void OnIsSyncEnabledChanged(bool value) + { + if (value) + { + StartSync(); + } + else + { + StopSync(); + } + } + + private void StartSync() + { + try + { + dittoManager.StartSync(); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error starting Ditto sync: {e.Message}"); + } + } + + private void StopSync() + { + try + { + dittoManager.StopSync(); + } + catch (Exception e) + { + logger.LogError($"TasksPageviewModel: Error stopping Ditto sync: {e.Message}"); + } + } + } +} diff --git a/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml b/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml index c68c9bdce..951cbd4fc 100644 --- a/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml +++ b/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml @@ -1,93 +1,93 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml.cs b/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml.cs index 32efb54f0..45e217385 100644 --- a/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml.cs +++ b/dotnet-maui/DittoMauiTasksApp/Views/TasksPage.xaml.cs @@ -1,31 +1,31 @@ -using DittoMauiTasksApp.ViewModels; - -namespace DittoMauiTasksApp; - -public partial class TasksPage : ContentPage -{ - public TasksPage(TasksPageviewModel viewModel) - { - InitializeComponent(); - - BindingContext = viewModel; - } - - // Event handler invoked when a checkbox on the page is checked or unchecked. - private void OnCheckBoxCheckedChanged(object sender, CheckedChangedEventArgs e) - { - if (sender is CheckBox checkBox && checkBox.BindingContext is DittoTask task) - { - if (!checkBox.IsVisible || !checkBox.IsEnabled) - { - return; - } - - var viewModel = BindingContext as TasksPageviewModel; - if (viewModel?.UpdateTaskDoneCommand.CanExecute(task) == true) - { - viewModel.UpdateTaskDoneCommand.Execute(task); - } - } - } -} +using DittoMauiTasksApp.ViewModels; + +namespace DittoMauiTasksApp; + +public partial class TasksPage : ContentPage +{ + public TasksPage(TasksPageviewModel viewModel) + { + InitializeComponent(); + + BindingContext = viewModel; + } + + // Event handler invoked when a checkbox on the page is checked or unchecked. + private void OnCheckBoxCheckedChanged(object sender, CheckedChangedEventArgs e) + { + if (sender is CheckBox checkBox && checkBox.BindingContext is TaskModel task) + { + if (!checkBox.IsVisible || !checkBox.IsEnabled) + { + return; + } + + var viewModel = BindingContext as TasksPageviewModel; + if (viewModel?.UpdateTaskDoneCommand.CanExecute(task) == true) + { + viewModel.UpdateTaskDoneCommand.Execute(task); + } + } + } +} diff --git a/dotnet-maui/README.md b/dotnet-maui/README.md index 62a3e5885..e74b32c51 100644 --- a/dotnet-maui/README.md +++ b/dotnet-maui/README.md @@ -6,17 +6,17 @@ 2. Install the .NET MAUI workload by running `dotnet workload install maui`. 3. If you want to build and test the MAUI app for iOS, install Xcode from the Mac App Store. 4. If you want to build and test the MAUI app for Android, install Android Studio, or install the Android SDK, Java JDK, and Android emulator. -5. Create an application at . Make note of the app ID and online playground token -6. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your app ID and online playground token. +5. Create an application at . Make note of the Database ID and development token +6. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your Database ID and development token. -### Environment variables (Android builds and tests) +### Environment variables (Android builds) -The .NET Android SDK requires `JAVA_HOME` and (for the UI tests / Appium) `ANDROID_HOME`. +The .NET Android SDK requires `JAVA_HOME` and `ANDROID_HOME`. ## Documentation - [Ditto C# .NET SDK Install Guide](https://docs.ditto.live/install-guides/c-sharp) -- [Ditto C# .NET SDK API Reference](https://software.ditto.live/dotnet/Ditto/5.0.0/api-reference/) +- [Ditto C# .NET SDK API Reference](https://docs.ditto.live/sdk/latest/api-reference/c-sharp) ### Restore Packages ```sh @@ -61,53 +61,3 @@ dotnet build -t:Run -f net10.0-windows10.0.19041.0 ### Other MAUI Platforms Other platforms not supported at this time. - -## Running the UI Tests Locally - -The `UITests.iOS` and `UITests.Android` projects are Appium-driven UI tests run via `dotnet run` (Program.cs is the entry point). They require: - -1. **Appium 2 + drivers**: - ```sh - npm install -g appium - appium driver install xcuitest - appium driver install uiautomator2 - ``` -2. **Appium server** running with `ANDROID_HOME` and `JAVA_HOME` exported in *its* shell: - ```sh - appium - ``` -3. **A booted simulator/emulator** (or a connected device). -4. **The app installed on the target** before `dotnet run`. -5. **`DITTO_CLOUD_TASK_TITLE`** set to a task title that exists in your Ditto cloud (the test asserts it can be found in the synced UI). - -### iOS - -```sh -# Build & install to the booted simulator -dotnet build DittoMauiTasksApp/DittoMauiTasksApp.csproj \ - -f net10.0-ios -p:ValidateXcodeVersion=false -t:Run - -# Run the test -cd UITests.iOS -DITTO_CLOUD_TASK_TITLE="" dotnet run -``` - -### Android - -The Debug APK uses Mono Fast Deployment by default, which only works when launched via `dotnet build -t:Run`. If you `adb install` the APK manually, build with assemblies embedded: - -```sh -dotnet build DittoMauiTasksApp/DittoMauiTasksApp.csproj \ - -f net10.0-android -p:EmbedAssembliesIntoApk=true - -adb -s emulator-5554 install -r \ - DittoMauiTasksApp/bin/Debug/net10.0-android/live.ditto.quickstart.mauitasksapp-Signed.apk -``` - -When multiple Android devices are connected (e.g., an emulator + an unauthorized physical device), point the test at a specific one with `APPIUM_UDID`: - -```sh -cd UITests.Android -APPIUM_UDID="emulator-5554" \ -DITTO_CLOUD_TASK_TITLE="" dotnet run -``` diff --git a/dotnet-maui/UITests.Android/AndroidTaskSearchTests.cs b/dotnet-maui/UITests.Android/AndroidTaskSearchTests.cs index f8f6b886a..6eb6eaebe 100644 --- a/dotnet-maui/UITests.Android/AndroidTaskSearchTests.cs +++ b/dotnet-maui/UITests.Android/AndroidTaskSearchTests.cs @@ -1,86 +1,86 @@ -using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Appium.Android; -using UITests.Shared; - -namespace UITests.Android; - -public class AndroidTaskSearchTests : TaskSearchTests -{ - protected override AppiumDriver CreateDriver() - { - var options = new AppiumOptions(); - - // Check if running on BrowserStack - var browserstackUsername = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME"); - var browserstackAccessKey = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY"); - var browserstackApp = Environment.GetEnvironmentVariable("BROWSERSTACK_APP_ID"); - - if (!string.IsNullOrEmpty(browserstackUsername) && !string.IsNullOrEmpty(browserstackAccessKey)) - { - // Load device config from environment (set by workflow from browserstack-devices.json) - var deviceString = Environment.GetEnvironmentVariable("BROWSERSTACK_DEVICE"); - if (string.IsNullOrEmpty(deviceString)) - { - throw new InvalidOperationException("BROWSERSTACK_DEVICE environment variable must be set"); - } - var deviceParts = deviceString.Split('-'); - var deviceName = deviceParts[0]; - var platformVersion = deviceParts.Length > 1 ? deviceParts[1] : throw new InvalidOperationException("Device string must include platform version (e.g., 'Google Pixel 7-13.0')"); - - // BrowserStack capabilities - options.PlatformName = "Android"; - options.AutomationName = "UiAutomator2"; - options.DeviceName = deviceName; - options.PlatformVersion = platformVersion; - options.App = browserstackApp ?? GetAppPath(); - options.AddAdditionalAppiumOption("project", "QuickStart .NET MAUI"); - options.AddAdditionalAppiumOption("build", Environment.GetEnvironmentVariable("BUILD_NAME") ?? "Local Tests"); - options.AddAdditionalAppiumOption("name", "Android Task Search Tests"); - - var uri = new Uri($"https://{browserstackUsername}:{browserstackAccessKey}@hub-cloud.browserstack.com/wd/hub"); - return new AndroidDriver(uri, options); - } - else - { - // Local testing capabilities - options.PlatformName = "Android"; - options.AutomationName = "UIAutomator2"; - options.App = GetAppPath(); - - // Optional capabilities for local testing - options.AddAdditionalAppiumOption("appWaitActivity", "crc647fcdc6dfabca042e.MainActivity"); - options.AddAdditionalAppiumOption("newCommandTimeout", 300); - options.AddAdditionalAppiumOption("autoGrantPermissions", true); - - // Optional: target a specific device when multiple are connected (e.g. APPIUM_UDID=emulator-5554) - var explicitUdid = Environment.GetEnvironmentVariable("APPIUM_UDID"); - if (!string.IsNullOrEmpty(explicitUdid)) - { - options.AddAdditionalAppiumOption("udid", explicitUdid); - } - - return new AndroidDriver(new Uri("http://127.0.0.1:4723"), options); - } - } - - private string GetAppPath() - { - var projectRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..")); - var apkFileName = "live.ditto.quickstart.mauitasksapp-Signed.apk"; - - // Look for APK in release directory first (what we just built) - var releasePath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Release", "net10.0-android", apkFileName); - if (File.Exists(releasePath)) - { - return releasePath; - } - - var debugPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-android", apkFileName); - if (File.Exists(debugPath)) - { - return debugPath; - } - - throw new FileNotFoundException($"APK not found. Checked:\n- {debugPath}\n- {releasePath}"); - } -} \ No newline at end of file +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Android; +using UITests.Shared; + +namespace UITests.Android; + +public class AndroidTaskSearchTests : TaskSearchTests +{ + protected override AppiumDriver CreateDriver() + { + var options = new AppiumOptions(); + + // Check if running on BrowserStack + var browserstackUsername = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME"); + var browserstackAccessKey = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY"); + var browserstackApp = Environment.GetEnvironmentVariable("BROWSERSTACK_APP_ID"); + + if (!string.IsNullOrEmpty(browserstackUsername) && !string.IsNullOrEmpty(browserstackAccessKey)) + { + // Load device config from environment (set by workflow from browserstack-devices.json) + var deviceString = Environment.GetEnvironmentVariable("BROWSERSTACK_DEVICE"); + if (string.IsNullOrEmpty(deviceString)) + { + throw new InvalidOperationException("BROWSERSTACK_DEVICE environment variable must be set"); + } + var deviceParts = deviceString.Split('-'); + var deviceName = deviceParts[0]; + var platformVersion = deviceParts.Length > 1 ? deviceParts[1] : throw new InvalidOperationException("Device string must include platform version (e.g., 'Google Pixel 7-13.0')"); + + // BrowserStack capabilities + options.PlatformName = "Android"; + options.AutomationName = "UiAutomator2"; + options.DeviceName = deviceName; + options.PlatformVersion = platformVersion; + options.App = browserstackApp ?? GetAppPath(); + options.AddAdditionalAppiumOption("project", "QuickStart .NET MAUI"); + options.AddAdditionalAppiumOption("build", Environment.GetEnvironmentVariable("BUILD_NAME") ?? "Local Tests"); + options.AddAdditionalAppiumOption("name", "Android Task Search Tests"); + + var uri = new Uri($"https://{browserstackUsername}:{browserstackAccessKey}@hub-cloud.browserstack.com/wd/hub"); + return new AndroidDriver(uri, options); + } + else + { + // Local testing capabilities + options.PlatformName = "Android"; + options.AutomationName = "UIAutomator2"; + options.App = GetAppPath(); + + // Optional capabilities for local testing + options.AddAdditionalAppiumOption("appWaitActivity", "crc647fcdc6dfabca042e.MainActivity"); + options.AddAdditionalAppiumOption("newCommandTimeout", 300); + options.AddAdditionalAppiumOption("autoGrantPermissions", true); + + // Optional: target a specific device when multiple are connected (e.g. APPIUM_UDID=emulator-5554) + var explicitUdid = Environment.GetEnvironmentVariable("APPIUM_UDID"); + if (!string.IsNullOrEmpty(explicitUdid)) + { + options.AddAdditionalAppiumOption("udid", explicitUdid); + } + + return new AndroidDriver(new Uri("http://127.0.0.1:4723"), options); + } + } + + private string GetAppPath() + { + var projectRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..")); + var apkFileName = "live.ditto.quickstart.mauitasksapp-Signed.apk"; + + // Look for APK in release directory first (what we just built) + var releasePath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Release", "net10.0-android", apkFileName); + if (File.Exists(releasePath)) + { + return releasePath; + } + + var debugPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-android", apkFileName); + if (File.Exists(debugPath)) + { + return debugPath; + } + + throw new FileNotFoundException($"APK not found. Checked:\n- {debugPath}\n- {releasePath}"); + } +} diff --git a/dotnet-maui/UITests.Android/Program.cs b/dotnet-maui/UITests.Android/Program.cs index a70d634d8..a8419ba28 100644 --- a/dotnet-maui/UITests.Android/Program.cs +++ b/dotnet-maui/UITests.Android/Program.cs @@ -33,4 +33,4 @@ { // Ignore teardown errors } -} \ No newline at end of file +} diff --git a/dotnet-maui/UITests.Shared/BaseTest.cs b/dotnet-maui/UITests.Shared/BaseTest.cs index 5d9bf849d..98f1be580 100644 --- a/dotnet-maui/UITests.Shared/BaseTest.cs +++ b/dotnet-maui/UITests.Shared/BaseTest.cs @@ -1,123 +1,123 @@ -using NUnit.Framework; -using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Appium.Android; -using OpenQA.Selenium.Appium.iOS; -using System.Reflection; - -namespace UITests.Shared; - -public abstract class BaseTest -{ - protected AppiumDriver App { get; set; } = null!; - private static bool _assemblyResolveHooked = false; - - protected abstract AppiumDriver CreateDriver(); - - [OneTimeSetUp] - public void OneTimeSetUp() - { - if (!_assemblyResolveHooked) - { - AppDomain.CurrentDomain.AssemblyResolve += ResolveAppiumAssembly; - _assemblyResolveHooked = true; - } - } - - private static Assembly? ResolveAppiumAssembly(object? sender, ResolveEventArgs args) - { - var assemblyName = new AssemblyName(args.Name); - - if (assemblyName.Name == "Appium.Net") - { - var nugetPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".nuget", "packages", "appium.webdriver", "5.2.0", "lib", "net6.0", "Appium.Net.dll"); - - if (File.Exists(nugetPath)) - { - return Assembly.LoadFrom(nugetPath); - } - - var localPath = Path.Combine(AppContext.BaseDirectory, "Appium.Net.dll"); - if (File.Exists(localPath)) - { - return Assembly.LoadFrom(localPath); - } - } - - return null; - } - - [SetUp] - public void SetUp() - { - App = CreateDriver(); - - // Handle permission dialogs on app startup - HandlePermissionDialogs(); - } - - private void HandlePermissionDialogs() - { - try - { - Thread.Sleep(2000); - var isIOS = App.GetType().Name.Contains("IOSDriver"); - - if (isIOS) - { - var iOSButtons = new[] { "Allow", "OK", "Allow While Using App", "Allow Once" }; - - for (int dialogRound = 1; dialogRound <= 2; dialogRound++) - { - foreach (var buttonText in iOSButtons) - { - try - { - var allowButton = App.FindElement(MobileBy.XPath($"//XCUIElementTypeButton[@name='{buttonText}']")); - if (allowButton.Displayed) - { - allowButton.Click(); - Thread.Sleep(3000); - break; - } - } - catch - { - } - } - Thread.Sleep(1000); - } - } - else - { - var androidButtons = new[] { "Allow", "ALLOW", "OK", "Accept" }; - - foreach (var buttonText in androidButtons) - { - try - { - var allowButton = App.FindElement(MobileBy.XPath($"//android.widget.Button[@text='{buttonText}']")); - if (allowButton.Displayed) - { - allowButton.Click(); - Thread.Sleep(1000); - } - } - catch - { - } - } - } - } - catch - { - } - } - - [TearDown] - public void TearDown() - { - App?.Quit(); - } -} \ No newline at end of file +using NUnit.Framework; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Android; +using OpenQA.Selenium.Appium.iOS; +using System.Reflection; + +namespace UITests.Shared; + +public abstract class BaseTest +{ + protected AppiumDriver App { get; set; } = null!; + private static bool _assemblyResolveHooked = false; + + protected abstract AppiumDriver CreateDriver(); + + [OneTimeSetUp] + public void OneTimeSetUp() + { + if (!_assemblyResolveHooked) + { + AppDomain.CurrentDomain.AssemblyResolve += ResolveAppiumAssembly; + _assemblyResolveHooked = true; + } + } + + private static Assembly? ResolveAppiumAssembly(object? sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + + if (assemblyName.Name == "Appium.Net") + { + var nugetPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".nuget", "packages", "appium.webdriver", "5.2.0", "lib", "net6.0", "Appium.Net.dll"); + + if (File.Exists(nugetPath)) + { + return Assembly.LoadFrom(nugetPath); + } + + var localPath = Path.Combine(AppContext.BaseDirectory, "Appium.Net.dll"); + if (File.Exists(localPath)) + { + return Assembly.LoadFrom(localPath); + } + } + + return null; + } + + [SetUp] + public void SetUp() + { + App = CreateDriver(); + + // Handle permission dialogs on app startup + HandlePermissionDialogs(); + } + + private void HandlePermissionDialogs() + { + try + { + Thread.Sleep(2000); + var isIOS = App.GetType().Name.Contains("IOSDriver"); + + if (isIOS) + { + var iOSButtons = new[] { "Allow", "OK", "Allow While Using App", "Allow Once" }; + + for (int dialogRound = 1; dialogRound <= 2; dialogRound++) + { + foreach (var buttonText in iOSButtons) + { + try + { + var allowButton = App.FindElement(MobileBy.XPath($"//XCUIElementTypeButton[@name='{buttonText}']")); + if (allowButton.Displayed) + { + allowButton.Click(); + Thread.Sleep(3000); + break; + } + } + catch + { + } + } + Thread.Sleep(1000); + } + } + else + { + var androidButtons = new[] { "Allow", "ALLOW", "OK", "Accept" }; + + foreach (var buttonText in androidButtons) + { + try + { + var allowButton = App.FindElement(MobileBy.XPath($"//android.widget.Button[@text='{buttonText}']")); + if (allowButton.Displayed) + { + allowButton.Click(); + Thread.Sleep(1000); + } + } + catch + { + } + } + } + } + catch + { + } + } + + [TearDown] + public void TearDown() + { + App?.Quit(); + } +} diff --git a/dotnet-maui/UITests.Shared/TaskSearchTests.cs b/dotnet-maui/UITests.Shared/TaskSearchTests.cs index fb53c12d1..ebf095104 100644 --- a/dotnet-maui/UITests.Shared/TaskSearchTests.cs +++ b/dotnet-maui/UITests.Shared/TaskSearchTests.cs @@ -1,87 +1,87 @@ -using NUnit.Framework; -using OpenQA.Selenium; -using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Support.UI; - -namespace UITests.Shared; - -[TestFixture] -public abstract class TaskSearchTests : BaseTest -{ - [Test] - public void CanFindTaskByTitle() - { - var expectedTaskTitle = Environment.GetEnvironmentVariable("DITTO_CLOUD_TASK_TITLE"); - - if (string.IsNullOrEmpty(expectedTaskTitle)) - { - Assert.Fail("DITTO_CLOUD_TASK_TITLE environment variable is not set or empty"); - return; - } - - var isBrowserStack = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME")); - - try - { - // Wait for app to load and sync - longer for BrowserStack - var waitTime = isBrowserStack ? 15000 : 6000; - Thread.Sleep(waitTime); - - var taskFound = false; - - for (int attempt = 1; attempt <= 10; attempt++) - { - try - { - // Find all TaskTitleLabel elements using AutomationId (works on both iOS and Android) - var taskLabels = App.FindElements(MobileBy.Id("TaskTitleLabel")); - - foreach (var label in taskLabels) - { - try - { - if (label.Text.Contains(expectedTaskTitle) && label.Displayed) - { - taskFound = true; - break; - } - } - catch - { - // Label not accessible, continue - } - } - - if (taskFound) - break; - } - catch - { - Thread.Sleep(1000); - } - } - - if (!taskFound) - { - if (isBrowserStack) - { - App.ExecuteScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"failed\", \"reason\": \"Task not found\"}}"); - } - Assert.Fail($"Task '{expectedTaskTitle}' not found"); - } - - if (isBrowserStack) - { - App.ExecuteScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"passed\", \"reason\": \"Task found successfully\"}}"); - } - } - catch (Exception ex) - { - if (isBrowserStack) - { - App.ExecuteScript($"browserstack_executor: {{\"action\": \"setSessionStatus\", \"arguments\": {{\"status\": \"failed\", \"reason\": \"{ex.Message}\"}}}}"); - } - Assert.Fail($"Test failed: {ex.Message}"); - } - } -} \ No newline at end of file +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Support.UI; + +namespace UITests.Shared; + +[TestFixture] +public abstract class TaskSearchTests : BaseTest +{ + [Test] + public void CanFindTaskByTitle() + { + var expectedTaskTitle = Environment.GetEnvironmentVariable("DITTO_CLOUD_TASK_TITLE"); + + if (string.IsNullOrEmpty(expectedTaskTitle)) + { + Assert.Fail("DITTO_CLOUD_TASK_TITLE environment variable is not set or empty"); + return; + } + + var isBrowserStack = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME")); + + try + { + // Wait for app to load and sync - longer for BrowserStack + var waitTime = isBrowserStack ? 15000 : 6000; + Thread.Sleep(waitTime); + + var taskFound = false; + + for (int attempt = 1; attempt <= 10; attempt++) + { + try + { + // Find all TaskTitleLabel elements using AutomationId (works on both iOS and Android) + var taskLabels = App.FindElements(MobileBy.Id("TaskTitleLabel")); + + foreach (var label in taskLabels) + { + try + { + if (label.Text.Contains(expectedTaskTitle) && label.Displayed) + { + taskFound = true; + break; + } + } + catch + { + // Label not accessible, continue + } + } + + if (taskFound) + break; + } + catch + { + Thread.Sleep(1000); + } + } + + if (!taskFound) + { + if (isBrowserStack) + { + App.ExecuteScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"failed\", \"reason\": \"Task not found\"}}"); + } + Assert.Fail($"Task '{expectedTaskTitle}' not found"); + } + + if (isBrowserStack) + { + App.ExecuteScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"passed\", \"reason\": \"Task found successfully\"}}"); + } + } + catch (Exception ex) + { + if (isBrowserStack) + { + App.ExecuteScript($"browserstack_executor: {{\"action\": \"setSessionStatus\", \"arguments\": {{\"status\": \"failed\", \"reason\": \"{ex.Message}\"}}}}"); + } + Assert.Fail($"Test failed: {ex.Message}"); + } + } +} diff --git a/dotnet-maui/UITests.iOS/Program.cs b/dotnet-maui/UITests.iOS/Program.cs index d6be342cd..9ada388c7 100644 --- a/dotnet-maui/UITests.iOS/Program.cs +++ b/dotnet-maui/UITests.iOS/Program.cs @@ -33,4 +33,4 @@ { // Ignore teardown errors } -} \ No newline at end of file +} diff --git a/dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs b/dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs index bc9a69e06..4a0d46ca5 100644 --- a/dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs +++ b/dotnet-maui/UITests.iOS/iOSTaskSearchTests.cs @@ -1,127 +1,127 @@ -using OpenQA.Selenium.Appium; -using OpenQA.Selenium.Appium.iOS; -using UITests.Shared; -using System.Diagnostics; -using System.Text.RegularExpressions; - -namespace UITests.iOS; - -public class iOSTaskSearchTests : TaskSearchTests -{ - protected override AppiumDriver CreateDriver() - { - var options = new AppiumOptions(); - - // Check if running on BrowserStack - var browserstackUsername = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME"); - var browserstackAccessKey = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY"); - var browserstackApp = Environment.GetEnvironmentVariable("BROWSERSTACK_APP_ID"); - - if (!string.IsNullOrEmpty(browserstackUsername) && !string.IsNullOrEmpty(browserstackAccessKey)) - { - // Load device config from environment (set by workflow from browserstack-devices.json) - var deviceString = Environment.GetEnvironmentVariable("BROWSERSTACK_DEVICE"); - if (string.IsNullOrEmpty(deviceString)) - { - throw new InvalidOperationException("BROWSERSTACK_DEVICE environment variable must be set"); - } - var deviceParts = deviceString.Split('-'); - var deviceName = deviceParts[0]; - var platformVersion = deviceParts.Length > 1 ? deviceParts[1] : throw new InvalidOperationException("Device string must include platform version (e.g., 'iPhone 15-17.0')"); - - // BrowserStack capabilities for iOS - options.PlatformName = "iOS"; - options.AutomationName = "XCUITest"; - options.DeviceName = deviceName; - options.PlatformVersion = platformVersion; - options.App = browserstackApp ?? GetAppPath(); - options.AddAdditionalAppiumOption("project", "QuickStart .NET MAUI"); - options.AddAdditionalAppiumOption("build", Environment.GetEnvironmentVariable("BUILD_NAME") ?? "Local Tests"); - options.AddAdditionalAppiumOption("name", "iOS Task Search Tests"); - options.AddAdditionalAppiumOption("autoGrantPermissions", true); - - var uri = new Uri($"https://{browserstackUsername}:{browserstackAccessKey}@hub-cloud.browserstack.com/wd/hub"); - return new IOSDriver(uri, options); - } - else - { - // Local testing capabilities - options.PlatformName = "iOS"; - options.AutomationName = "XCUITest"; - - // App capabilities - use the built .app - options.App = Path.Combine(GetAppPath(), "DittoMauiTasksApp.app"); - - // iOS Simulator capabilities - automatically detect booted simulator - var (deviceName, platformVersion) = GetFirstBootedSimulator(); - options.DeviceName = deviceName; - options.PlatformVersion = platformVersion; - options.AddAdditionalAppiumOption("newCommandTimeout", 300); - return new IOSDriver(new Uri("http://127.0.0.1:4723"), options); - } - } - - private string GetAppPath() - { - // Look for the .app in the MAUI project's build output - var projectRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..")); - var appPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-ios", "iossimulator-arm64"); - - if (!Directory.Exists(appPath)) - { - // Fallback to x64 simulator - appPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-ios", "iossimulator-x64"); - } - - return appPath; - } - - private static (string DeviceName, string PlatformVersion) GetFirstBootedSimulator() - { - var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = "xcrun", - Arguments = "simctl list devices", - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true - } - }; - - process.Start(); - var output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - // Parse output to find first booted device - var lines = output.Split('\n'); - string? currentRuntime = null; - - foreach (var line in lines) - { - // Check if line contains runtime version (e.g. "-- iOS 18.6 --") - var runtimeMatch = Regex.Match(line, @"-- iOS ([\d.]+) --"); - if (runtimeMatch.Success) - { - currentRuntime = runtimeMatch.Groups[1].Value; - continue; - } - - // Check if line contains a booted device - if (line.Contains("(Booted)") && currentRuntime != null) - { - // Extract device name (e.g. "iPhone 16 (8E5063C4-E64F-46BD-B445-1B22F85B3310) (Booted)") - var deviceMatch = Regex.Match(line, @"^\s+([^(]+)\s+\([^)]+\)\s+\(Booted\)"); - if (deviceMatch.Success) - { - var deviceName = deviceMatch.Groups[1].Value.Trim(); - return (deviceName, currentRuntime); - } - } - } - - // Fallback if no booted simulator found - throw new InvalidOperationException("No booted iOS simulators found. Please boot a simulator first."); - } -} \ No newline at end of file +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.iOS; +using UITests.Shared; +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace UITests.iOS; + +public class iOSTaskSearchTests : TaskSearchTests +{ + protected override AppiumDriver CreateDriver() + { + var options = new AppiumOptions(); + + // Check if running on BrowserStack + var browserstackUsername = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME"); + var browserstackAccessKey = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY"); + var browserstackApp = Environment.GetEnvironmentVariable("BROWSERSTACK_APP_ID"); + + if (!string.IsNullOrEmpty(browserstackUsername) && !string.IsNullOrEmpty(browserstackAccessKey)) + { + // Load device config from environment (set by workflow from browserstack-devices.json) + var deviceString = Environment.GetEnvironmentVariable("BROWSERSTACK_DEVICE"); + if (string.IsNullOrEmpty(deviceString)) + { + throw new InvalidOperationException("BROWSERSTACK_DEVICE environment variable must be set"); + } + var deviceParts = deviceString.Split('-'); + var deviceName = deviceParts[0]; + var platformVersion = deviceParts.Length > 1 ? deviceParts[1] : throw new InvalidOperationException("Device string must include platform version (e.g., 'iPhone 15-17.0')"); + + // BrowserStack capabilities for iOS + options.PlatformName = "iOS"; + options.AutomationName = "XCUITest"; + options.DeviceName = deviceName; + options.PlatformVersion = platformVersion; + options.App = browserstackApp ?? GetAppPath(); + options.AddAdditionalAppiumOption("project", "QuickStart .NET MAUI"); + options.AddAdditionalAppiumOption("build", Environment.GetEnvironmentVariable("BUILD_NAME") ?? "Local Tests"); + options.AddAdditionalAppiumOption("name", "iOS Task Search Tests"); + options.AddAdditionalAppiumOption("autoGrantPermissions", true); + + var uri = new Uri($"https://{browserstackUsername}:{browserstackAccessKey}@hub-cloud.browserstack.com/wd/hub"); + return new IOSDriver(uri, options); + } + else + { + // Local testing capabilities + options.PlatformName = "iOS"; + options.AutomationName = "XCUITest"; + + // App capabilities - use the built .app + options.App = Path.Combine(GetAppPath(), "DittoMauiTasksApp.app"); + + // iOS Simulator capabilities - automatically detect booted simulator + var (deviceName, platformVersion) = GetFirstBootedSimulator(); + options.DeviceName = deviceName; + options.PlatformVersion = platformVersion; + options.AddAdditionalAppiumOption("newCommandTimeout", 300); + return new IOSDriver(new Uri("http://127.0.0.1:4723"), options); + } + } + + private string GetAppPath() + { + // Look for the .app in the MAUI project's build output + var projectRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..")); + var appPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-ios", "iossimulator-arm64"); + + if (!Directory.Exists(appPath)) + { + // Fallback to x64 simulator + appPath = Path.Combine(projectRoot, "DittoMauiTasksApp", "bin", "Debug", "net10.0-ios", "iossimulator-x64"); + } + + return appPath; + } + + private static (string DeviceName, string PlatformVersion) GetFirstBootedSimulator() + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "xcrun", + Arguments = "simctl list devices", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + } + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + // Parse output to find first booted device + var lines = output.Split('\n'); + string? currentRuntime = null; + + foreach (var line in lines) + { + // Check if line contains runtime version (e.g. "-- iOS 18.6 --") + var runtimeMatch = Regex.Match(line, @"-- iOS ([\d.]+) --"); + if (runtimeMatch.Success) + { + currentRuntime = runtimeMatch.Groups[1].Value; + continue; + } + + // Check if line contains a booted device + if (line.Contains("(Booted)") && currentRuntime != null) + { + // Extract device name (e.g. "iPhone 16 (8E5063C4-E64F-46BD-B445-1B22F85B3310) (Booted)") + var deviceMatch = Regex.Match(line, @"^\s+([^(]+)\s+\([^)]+\)\s+\(Booted\)"); + if (deviceMatch.Success) + { + var deviceName = deviceMatch.Groups[1].Value.Trim(); + return (deviceName, currentRuntime); + } + } + } + + // Fallback if no booted simulator found + throw new InvalidOperationException("No booted iOS simulators found. Please boot a simulator first."); + } +} diff --git a/dotnet-tui/DittoDotNetTasksConsole.Tests/DittoDotNetTasksConsole.Tests.csproj b/dotnet-tui/DittoDotNetTasksConsole.Tests/DittoDotNetTasksConsole.Tests.csproj index 9eb14b0ed..02a2c4a1b 100644 --- a/dotnet-tui/DittoDotNetTasksConsole.Tests/DittoDotNetTasksConsole.Tests.csproj +++ b/dotnet-tui/DittoDotNetTasksConsole.Tests/DittoDotNetTasksConsole.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/dotnet-tui/DittoDotNetTasksConsole.Tests/IntegrationTests.cs b/dotnet-tui/DittoDotNetTasksConsole.Tests/IntegrationTests.cs index d71fc65a4..c075474f9 100644 --- a/dotnet-tui/DittoDotNetTasksConsole.Tests/IntegrationTests.cs +++ b/dotnet-tui/DittoDotNetTasksConsole.Tests/IntegrationTests.cs @@ -11,21 +11,21 @@ namespace DittoDotNetTasksConsole.Tests; public class IntegrationTests { [Fact] - public async Task TasksPeer_CanSyncAndRetrieveTasks() + public async Task TasksRepository_CanSyncAndRetrieveTasks() { var env = LoadEnvVariables(); var taskToFind = Environment.GetEnvironmentVariable("DITTO_CLOUD_TASK_TITLE") ?? throw new InvalidOperationException("DITTO_CLOUD_TASK_TITLE environment variable is required"); - using var peer = await TasksPeer.Create( - env["DITTO_APP_ID"], - env["DITTO_PLAYGROUND_TOKEN"], - env["DITTO_AUTH_URL"], - env["DITTO_WEBSOCKET_URL"]); + using var dittoManager = await DittoManager.Create( + env["DITTO_DATABASE_ID"], + env["DITTO_DEVELOPMENT_TOKEN"], + env["DITTO_SERVER_URL"]); + var tasksRepository = new TasksRepository(dittoManager); // Verify connection - Assert.NotNull(peer); - Assert.Equal(env["DITTO_APP_ID"], peer.AppId); + Assert.NotNull(dittoManager); + Assert.Equal(env["DITTO_DATABASE_ID"], dittoManager.DatabaseId); var foundTask = false; var attempts = 0; @@ -36,8 +36,8 @@ public async Task TasksPeer_CanSyncAndRetrieveTasks() await Task.Delay(1000); attempts++; - var tasksList = new List(); - var observer = peer.ObserveTasksCollection(tasks => + var tasksList = new List(); + var observer = tasksRepository.ObserveTasksCollection(tasks => { tasksList.AddRange(tasks); return Task.CompletedTask; @@ -89,4 +89,4 @@ private static Dictionary LoadEnvVariables() return envVars; } -} \ No newline at end of file +} diff --git a/dotnet-tui/DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj b/dotnet-tui/DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj index 6cef927a6..466e85620 100644 --- a/dotnet-tui/DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj +++ b/dotnet-tui/DittoDotNetTasksConsole/DittoDotNetTasksConsole.csproj @@ -9,7 +9,7 @@ - + diff --git a/dotnet-tui/DittoDotNetTasksConsole/DittoManager.cs b/dotnet-tui/DittoDotNetTasksConsole/DittoManager.cs new file mode 100644 index 000000000..bfb0d5685 --- /dev/null +++ b/dotnet-tui/DittoDotNetTasksConsole/DittoManager.cs @@ -0,0 +1,114 @@ +using System; +using System.Threading.Tasks; + +using DittoSDK; +using DittoSDK.Auth; + +/// +/// Manages the lifecycle and configuration of a Ditto instance: config, open, +/// auth, and starting/stopping sync. It vends a configured, already-running +/// instance (via the property) for the +/// tasks repository to use directly. +/// +/// +/// This class deliberately does NOT wrap Ditto's APIs (store/sync/etc.). The +/// repository calls the real Ditto API — e.g. dittoManager.Ditto.Store.ExecuteAsync(...) +/// — so the quickstart shows how to use Ditto directly rather than modeling an +/// abstraction layer over it. +/// +public class DittoManager : IDisposable +{ + public string DatabaseId { get; private set; } + public string DevelopmentToken { get; private set; } + public string ServerUrl { get; private set; } + + public bool IsSyncActive => _ditto.Sync.IsActive; + + private readonly Ditto _ditto; + + /// + /// The configured, already-running Ditto instance. The tasks repository uses + /// this to call the real Ditto API directly. + /// + public Ditto Ditto => _ditto; + + /// + /// Creates a new synchronizing DittoManager instance. + /// + public static Task Create( + string databaseId, + string developmentToken, + string serverUrl) + { + var dittoManager = new DittoManager(databaseId, developmentToken, serverUrl); + dittoManager.Authenticate(); + dittoManager.StartSync(); + + return Task.FromResult(dittoManager); + } + + private void Authenticate() + { + _ditto.Auth.ExpirationHandler = async (ditto, secondsRemaining) => + { + // Authenticate when token is expiring + try + { + await ditto.Auth.LoginAsync( + // Your development token, replace with your actual token + DevelopmentToken, + // Use DittoAuthenticationProvider.Development, or your actual provider + DittoAuthenticationProvider.Development + ); + } + catch (Exception error) + { + Console.WriteLine($"Authentication failed: {error}"); + } + }; + } + + /// + /// Constructor + /// + /// Ditto database ID + /// Ditto development token + /// Ditto Server URL + private DittoManager(string databaseId, string developmentToken, string serverUrl) + { + DatabaseId = databaseId; + DevelopmentToken = developmentToken; + ServerUrl = serverUrl; + + var config = new DittoConfig( + DatabaseId, + new DittoConfigConnect.Server(new Uri(serverUrl)) + ); + + _ditto = Ditto.Open(config); + } + + public void Dispose() + { + _ditto.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// Start synchronizing with other peers and the Ditto server. + /// + public void StartSync() + { + _ditto.Sync.Start(); + } + + /// + /// Stop synchronizing. + /// + public void StopSync() + { + // Stop the sync engine only. The subscription is owned by TasksRepository + // and stays registered across a sync on/off toggle. + _ditto.Sync.Stop(); + } +} diff --git a/dotnet-tui/DittoDotNetTasksConsole/Program.cs b/dotnet-tui/DittoDotNetTasksConsole/Program.cs index 258fcd308..6948e7007 100644 --- a/dotnet-tui/DittoDotNetTasksConsole/Program.cs +++ b/dotnet-tui/DittoDotNetTasksConsole/Program.cs @@ -16,17 +16,17 @@ public static async Task Main(string[] args) try { var env = LoadEnvVariables(); - var appId = env["DITTO_APP_ID"]; - var playgroundToken = env["DITTO_PLAYGROUND_TOKEN"]; - var websocketUrl = env["DITTO_WEBSOCKET_URL"]; - var authUrl = env["DITTO_AUTH_URL"]; + var databaseId = env["DITTO_DATABASE_ID"]; + var developmentToken = env["DITTO_DEVELOPMENT_TOKEN"]; + var serverUrl = env["DITTO_SERVER_URL"]; - using var peer = await TasksPeer.Create(appId, playgroundToken, authUrl, websocketUrl); + using var dittoManager = await DittoManager.Create(databaseId, developmentToken, serverUrl); + var tasksRepository = new TasksRepository(dittoManager); // Disable Ditto's standard-error logging, which would interfere // with the the Terminal.Gui UI. DittoLogger.IsEnabled = false; - RunTerminalGui(peer); + RunTerminalGui(dittoManager, tasksRepository); } catch (Exception ex) { @@ -34,12 +34,12 @@ public static async Task Main(string[] args) } } - private static void RunTerminalGui(TasksPeer peer) + private static void RunTerminalGui(DittoManager dittoManager, TasksRepository tasksRepository) { try { Application.Init(); - Application.Top.Add(new TasksWindow(peer)); + Application.Top.Add(new TasksWindow(dittoManager, tasksRepository)); // Sleep when idle, reducing CPU usage. Application.MainLoop.AddIdle(() => diff --git a/dotnet-tui/DittoDotNetTasksConsole/ToDoTask.cs b/dotnet-tui/DittoDotNetTasksConsole/TaskModel.cs similarity index 85% rename from dotnet-tui/DittoDotNetTasksConsole/ToDoTask.cs rename to dotnet-tui/DittoDotNetTasksConsole/TaskModel.cs index 0aef435b4..883d9be24 100644 --- a/dotnet-tui/DittoDotNetTasksConsole/ToDoTask.cs +++ b/dotnet-tui/DittoDotNetTasksConsole/TaskModel.cs @@ -5,10 +5,10 @@ /// Representation of a document in the Ditto 'tasks' collection. /// /// -/// This class is named ToDoTask rather than Task to avoid +/// This class is named TaskModel rather than Task to avoid /// conflicts with the System.Threading.Tasks.Task class. /// -public class ToDoTask +public class TaskModel { [JsonPropertyName("_id")] public string Id { get; set; } diff --git a/dotnet-tui/DittoDotNetTasksConsole/TasksPeer.cs b/dotnet-tui/DittoDotNetTasksConsole/TasksPeer.cs deleted file mode 100644 index 200fe4f4b..000000000 --- a/dotnet-tui/DittoDotNetTasksConsole/TasksPeer.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; - -using DittoSDK; -using DittoSDK.Auth; -using DittoSDK.Store; - -/// -/// Encapsulates use of the Ditto SDK and the 'tasks' collection. -/// -public class TasksPeer : IDisposable -{ - private const string Query = "SELECT * FROM tasks WHERE NOT deleted"; - - public string AppId { get; private set; } - public string PlaygroundToken { get; private set; } - public string AuthUrl { get; private set; } - public string WebsocketUrl { get; private set; } - - public bool IsSyncActive => _ditto.Sync.IsActive; - - private readonly Ditto _ditto; - - /// - /// Creates a new synchronizing TasksPeer instance. - /// - public static async Task Create( - string appId, - string playgroundToken, - string authUrl, - string websocketUrl) - { - var peer = new TasksPeer(appId, playgroundToken, authUrl, websocketUrl); - peer.Authenticate(); - peer.RegisterSubscription(); - await peer.InsertInitialTasks(); - peer.StartSync(); - - return peer; - } - - private void Authenticate() - { - _ditto.Auth.ExpirationHandler = async (ditto, secondsRemaining) => - { - // Authenticate when token is expiring - try - { - await ditto.Auth.LoginAsync( - // Your development token, replace with your actual token - PlaygroundToken, - // Use DittoAuthenticationProvider.Development for playground, or your actual provider - DittoAuthenticationProvider.Development - ); - } - catch (Exception error) - { - Console.WriteLine($"Authentication failed: {error}"); - } - }; - } - - /// - /// Registers a subscription for the tasks collection to enable data synchronization - /// with other peers and the Ditto cloud. This subscription determines what data - /// will be synced to this peer during the synchronization process. - /// - /// - /// The subscription is created using the same query that filters out deleted tasks - /// (SELECT * FROM tasks WHERE NOT deleted), ensuring that only active, - /// non-deleted tasks are synchronized across the network. - /// - /// This method should be called during peer initialization to establish the - /// subscription before starting the sync process. The subscription remains active - /// until explicitly cancelled or when the peer is disposed. - /// - /// - private void RegisterSubscription() - { - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - _ditto.Sync.RegisterSubscription(Query); - } - - /// - /// Constructor - /// - /// Ditto application ID - /// Ditto online playground token - /// Ditto Auth URL - /// Ditto Websocket URL - private TasksPeer(string appId, string playgroundToken, string authUrl, string websocketUrl) - { - AppId = appId; - PlaygroundToken = playgroundToken; - AuthUrl = authUrl; - WebsocketUrl = websocketUrl; - - var config = new DittoConfig( - AppId, - new DittoConfigConnect.Server(new Uri(authUrl)) - ); - - _ditto = Ditto.Open(config); - } - - public void Dispose() - { - _ditto.Dispose(); - GC.SuppressFinalize(this); - } - - /// - /// Inserts the initial tasks into the 'tasks' collection. - /// - private async Task InsertInitialTasks() - { - var initialTasks = new List> - { - new Dictionary - { - {"_id", "50191411-4C46-4940-8B72-5F8017A04FA7"}, - {"title", "Buy groceries"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "6DA283DA-8CFE-4526-A6FA-D385089364E5"}, - {"title", "Clean the kitchen"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "5303DDF8-0E72-4FEB-9E82-4B007E5797F0"}, - {"title", "Schedule dentist appointment"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "38411F1B-6B49-4346-90C3-0B16CE97E174"}, - {"title", "Pay bills"}, - {"done", false}, - {"deleted", false} - } - }; - - const string insertCommand = "INSERT INTO tasks INITIAL DOCUMENTS (:task)"; - foreach (var task in initialTasks) - { - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "task", task } - }); - } - } - - /// - /// Adds a new task to the 'tasks' collection. - /// - public async Task AddTask(string title) - { - if (string.IsNullOrWhiteSpace(title)) - { - throw new ArgumentException("title cannot be empty"); - } - - var doc = new Dictionary - { - {"title", title}, - {"done", false}, - {"deleted", false } - }; - const string insertCommand = "INSERT INTO tasks DOCUMENTS (:doc)"; - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "doc", doc } - }); - } - - /// - /// Update the title of an existing task. - /// - public async Task UpdateTaskTitle(string taskId, string newTitle) - { - if (string.IsNullOrWhiteSpace(taskId)) - { - throw new ArgumentException("taskId cannot be empty"); - } - - if (string.IsNullOrWhiteSpace(newTitle)) - { - throw new ArgumentException("title cannot be empty"); - } - - const string updateQuery = "UPDATE tasks SET title = :title WHERE _id = :id"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - {"title", newTitle}, - {"id", taskId} - }); - } - - /// - /// Mark a task as deleted. - /// - public async Task DeleteTask(string taskId) - { - if (string.IsNullOrWhiteSpace(taskId)) - { - throw new ArgumentException("taskId cannot be empty"); - } - - const string updateQuery = "UPDATE tasks SET deleted = true WHERE _id = :id"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - { "id", taskId } - }); - } - - /// - /// Mark a task as complete or not complete. - /// - public async Task UpdateTaskDone(string taskId, bool newDoneState) - { - const string updateQuery = "UPDATE tasks SET done = :newDoneState WHERE _id = :id AND done != :newDoneState"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary - { - { "newDoneState", newDoneState }, - { "id", taskId } - }); - } - - /// - /// Specify a handler to be called asynchronously when the task collection changes. - /// - public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) - { - // Register an observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - return _ditto.Store.RegisterObserver(Query, async (queryResult) => - { - try - { - // Deserialize the JSON documents into ToDoTask objects - var tasks = queryResult.Items.Select(d => - JsonSerializer.Deserialize(d.JsonString()) - ).OrderBy(t => t?.Id).ToList(); - - await handler(tasks); - } - catch (Exception e) - { - Console.Error.WriteLine($"ERROR tasks observation handler failed: {e.Message}"); - } - }); - } - - /// - /// Start synchronizing the 'tasks' collection. - /// - public void StartSync() - { - _ditto.Sync.Start(); - } - - /// - /// Stop synchronizing the 'tasks' collection. - /// - public void StopSync() - { - foreach (var subscription in _ditto.Sync.Subscriptions) - { - subscription.Cancel(); - } - _ditto.Sync.Stop(); - } -} \ No newline at end of file diff --git a/dotnet-tui/DittoDotNetTasksConsole/TasksRepository.cs b/dotnet-tui/DittoDotNetTasksConsole/TasksRepository.cs new file mode 100644 index 000000000..239c28868 --- /dev/null +++ b/dotnet-tui/DittoDotNetTasksConsole/TasksRepository.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +using DittoSDK.Store; + +/// +/// Owns all 'tasks' collection data operations: the DQL query strings, the sync +/// subscription, the store observer, and CRUD. It calls the real Ditto API +/// directly through the it is given. +/// +public class TasksRepository +{ + // The subscription controls what syncs to this device and is written to the + // local database; the observer reacts to changes in that local database, + // hiding soft-deleted tasks from the displayed list. + private const string SubscriptionQuery = "SELECT * FROM tasks"; + private const string ObserverQuery = "SELECT * FROM tasks WHERE NOT deleted"; + + private readonly DittoManager _dittoManager; + + public TasksRepository(DittoManager dittoManager) + { + _dittoManager = dittoManager; + RegisterSubscription(); + } + + /// + /// Registers a subscription for the tasks collection to enable data synchronization + /// with other peers and the Ditto server. This subscription determines what data + /// will be synced to this device during the synchronization process. + /// + /// + /// The subscription syncs all tasks (SELECT * FROM tasks) and controls + /// what is written to the local database; the observer query hides + /// soft-deleted tasks from the displayed list. + /// + /// + private void RegisterSubscription() + { + // Register a subscription, which determines what data syncs to this device + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + _dittoManager.Ditto.Sync.RegisterSubscription(SubscriptionQuery); + } + + /// + /// Adds a new task to the 'tasks' collection. + /// + public async Task AddTask(string title) + { + if (string.IsNullOrWhiteSpace(title)) + { + throw new ArgumentException("title cannot be empty"); + } + + var doc = new Dictionary + { + {"title", title}, + {"done", false}, + {"deleted", false } + }; + const string insertCommand = "INSERT INTO tasks DOCUMENTS (:task)"; + await _dittoManager.Ditto.Store.ExecuteAsync(insertCommand, new Dictionary() + { + { "task", doc } + }); + } + + /// + /// Update the title of an existing task. + /// + public async Task UpdateTaskTitle(string taskId, string newTitle) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + if (string.IsNullOrWhiteSpace(newTitle)) + { + throw new ArgumentException("title cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET title = :title WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + {"title", newTitle}, + {"id", taskId} + }); + } + + /// + /// Mark a task as deleted. + /// + public async Task DeleteTask(string taskId) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET deleted = true WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + { "id", taskId } + }); + } + + /// + /// Mark a task as complete or not complete. + /// + public async Task UpdateTaskDone(string taskId, bool newDoneState) + { + const string updateQuery = "UPDATE tasks SET done = :done WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary + { + { "done", newDoneState }, + { "id", taskId } + }); + } + + /// + /// Specify a handler to be called asynchronously when the task collection changes. + /// + public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) + { + // Register an observer, which runs against the local database on this device + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + return _dittoManager.Ditto.Store.RegisterObserver(ObserverQuery, async (queryResult) => + { + try + { + // Deserialize the JSON documents into TaskModel objects + var tasks = queryResult.Items.Select(d => + JsonSerializer.Deserialize(d.JsonString()) + ).ToList(); + + await handler(tasks); + } + catch (Exception e) + { + Console.Error.WriteLine($"ERROR tasks observation handler failed: {e.Message}"); + } + }); + } +} diff --git a/dotnet-tui/DittoDotNetTasksConsole/TasksWindow.cs b/dotnet-tui/DittoDotNetTasksConsole/TasksWindow.cs index 79b431d28..a38b71903 100644 --- a/dotnet-tui/DittoDotNetTasksConsole/TasksWindow.cs +++ b/dotnet-tui/DittoDotNetTasksConsole/TasksWindow.cs @@ -8,6 +8,8 @@ using Terminal.Gui.Graphs; using NStack; +using DittoSDK.Store; + /// /// A Terminal.Gui window that displays a list of tasks and allows the user to interact with them. /// @@ -18,12 +20,12 @@ public class TasksWindow : Window /// class TasksListDataSource : IListDataSource { - private readonly List _tasks = new(); - private readonly TasksPeer _peer; + private readonly List _tasks = new(); + private readonly TasksRepository _tasksRepository; - public TasksListDataSource(TasksPeer peer) + public TasksListDataSource(TasksRepository tasksRepository) { - _peer = peer; + _tasksRepository = tasksRepository; } public int Count => _tasks.Count; @@ -56,7 +58,7 @@ public void SetMark(int item, bool value) { try { - await _peer.UpdateTaskDone(id, value); + await _tasksRepository.UpdateTaskDone(id, value); } catch (Exception ex) { @@ -98,7 +100,7 @@ void RenderUstr(ConsoleDriver driver, ustring ustr, int col, int line, int width /// /// Get the task at the specified index, or null if the index is out of range. /// - public ToDoTask TaskAtIndex(int index) + public TaskModel TaskAtIndex(int index) { try { @@ -110,7 +112,7 @@ public ToDoTask TaskAtIndex(int index) } } - public void UpdateTasks(IList newTasks) + public void UpdateTasks(IList newTasks) { // Note: Simply replacing the collection with a new one will cause // the ListView to lose its selection state. So we update the @@ -142,13 +144,22 @@ public void UpdateTasks(IList newTasks) } } - private readonly TasksPeer _peer; + private readonly DittoManager _dittoManager; + private readonly TasksRepository _tasksRepository; private readonly TasksListDataSource _dataSource; - public TasksWindow(TasksPeer peer) : base($"Ditto Tasks") + /// + /// The tasks store observer. Held so it can be cancelled and disposed when + /// the window is disposed; otherwise it (and its Ditto callback) would leak + /// for the lifetime of the process. + /// + private DittoStoreObserver _tasksObserver; + + public TasksWindow(DittoManager dittoManager, TasksRepository tasksRepository) : base($"Ditto Tasks") { - _peer = peer; - _dataSource = new(peer); + _dittoManager = dittoManager; + _tasksRepository = tasksRepository; + _dataSource = new(tasksRepository); X = 0; Y = 0; @@ -165,13 +176,13 @@ public TasksWindow(TasksPeer peer) : base($"Ditto Tasks") }; Add(syncStatusLabel); - Add(new Label("App ID: " + _peer.AppId) + Add(new Label("Database ID: " + _dittoManager.DatabaseId) { X = Pos.Center(), Y = 1, }); - Add(new Label("Playground Token: " + _peer.PlaygroundToken) + Add(new Label("Development Token: " + _dittoManager.DevelopmentToken) { X = Pos.Center(), Y = 2, @@ -256,7 +267,7 @@ public TasksWindow(TasksPeer peer) : base($"Ditto Tasks") Add(tasksListView); - peer.ObserveTasksCollection(async (tasks) => + _tasksObserver = tasksRepository.ObserveTasksCollection(async (tasks) => { await Task.Run(() => { @@ -285,9 +296,24 @@ await Task.Run(() => }); } + /// + /// Cancel and dispose the tasks observer when the window is disposed so its + /// Ditto callback is not leaked. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _tasksObserver?.Cancel(); + _tasksObserver?.Dispose(); + _tasksObserver = null; + } + base.Dispose(disposing); + } + private string SyncStatusLabelText() { - return "Sync: " + (_peer.IsSyncActive ? "Active" : "Inactive") + return "Sync: " + (_dittoManager.IsSyncActive ? "Active" : "Inactive") + " (s: toggle)"; } @@ -303,7 +329,7 @@ private void HandleCreateCommand() { try { - await _peer.AddTask(title); + await _tasksRepository.AddTask(title); } catch (Exception ex) { @@ -313,7 +339,7 @@ private void HandleCreateCommand() } } - private void HandleDeleteCommand(ToDoTask task) + private void HandleDeleteCommand(TaskModel task) { var response = MessageBox.Query( "Delete Task", @@ -326,7 +352,7 @@ private void HandleDeleteCommand(ToDoTask task) { try { - await _peer.DeleteTask(id); + await _tasksRepository.DeleteTask(id); } catch (Exception ex) { @@ -336,7 +362,7 @@ private void HandleDeleteCommand(ToDoTask task) } } - private void HandleEditCommand(ToDoTask task) + private void HandleEditCommand(TaskModel task) { var originalTitle = task.Title; var newTitle = ShowTextInputDialog( @@ -351,7 +377,7 @@ private void HandleEditCommand(ToDoTask task) { try { - await _peer.UpdateTaskTitle(id, newTitle); + await _tasksRepository.UpdateTaskTitle(id, newTitle); } catch (Exception ex) { @@ -372,13 +398,13 @@ private void HandleQuitCommand() private void HandleToggleSyncCommand() { - if (_peer.IsSyncActive) + if (_dittoManager.IsSyncActive) { - _peer.StopSync(); + _dittoManager.StopSync(); } else { - _peer.StartSync(); + _dittoManager.StartSync(); } } diff --git a/dotnet-tui/README.md b/dotnet-tui/README.md index bf36e7a40..bfe31cdd5 100644 --- a/dotnet-tui/README.md +++ b/dotnet-tui/README.md @@ -4,14 +4,14 @@ ## Prerequisites 1. Install the .NET 10 SDK from -2. Create an application at . Make note of the app ID and online playground token -3. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your app ID and online playground token. +2. Create an application at . Make note of the Database ID and development token +3. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your Database ID and development token. ## Documentation - [Ditto C# .NET SDK Install Guide](https://docs.ditto.live/install-guides/c-sharp) -- [Ditto C# .NET SDK API Reference](https://software.ditto.live/dotnet/Ditto/5.0.0/api-reference/) +- [Ditto C# .NET SDK API Reference](https://docs.ditto.live/sdk/latest/api-reference/c-sharp) ## .NET Console App diff --git a/dotnet-winforms-net48/.env.sample b/dotnet-winforms-net48/.env.sample index a05d0fcf6..4740190e2 100644 --- a/dotnet-winforms-net48/.env.sample +++ b/dotnet-winforms-net48/.env.sample @@ -1,7 +1,7 @@ -#!/usr/bin/env bash - -# Copy this file from ".env.sample" to ".env", then fill in these values -# A Ditto AppID and Playground token can be obtained from https://portal.ditto.live -DITTO_APP_ID="" -DITTO_PLAYGROUND_TOKEN="" -DITTO_AUTH_URL="" \ No newline at end of file +#!/usr/bin/env bash + +# Copy this file from ".env.sample" to ".env", then fill in these values +# A Ditto Database ID and development token can be obtained from https://portal.ditto.live +DITTO_DATABASE_ID="" +DITTO_DEVELOPMENT_TOKEN="" +DITTO_SERVER_URL="" \ No newline at end of file diff --git a/dotnet-winforms-net48/App.config b/dotnet-winforms-net48/App.config index fcde7c8d9..533e4c0c2 100644 --- a/dotnet-winforms-net48/App.config +++ b/dotnet-winforms-net48/App.config @@ -1,18 +1,18 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet-winforms-net48/AppConfiguration.cs b/dotnet-winforms-net48/AppConfiguration.cs index 4fa69131f..75de2ffba 100644 --- a/dotnet-winforms-net48/AppConfiguration.cs +++ b/dotnet-winforms-net48/AppConfiguration.cs @@ -9,9 +9,9 @@ namespace Taskapp.WinForms.Net48 /// public static class AppConfiguration { - public static string AppId { get; private set; } - public static string PlaygroundToken { get; private set; } - public static string AuthUrl { get; private set; } + public static string DatabaseId { get; private set; } + public static string DevelopmentToken { get; private set; } + public static string ServerUrl { get; private set; } /// /// Loads configuration from .env file in the application directory @@ -42,26 +42,26 @@ public static void Load() switch (key) { - case "DITTO_APP_ID": - AppId = value; + case "DITTO_DATABASE_ID": + DatabaseId = value; break; - case "DITTO_PLAYGROUND_TOKEN": - PlaygroundToken = value; + case "DITTO_DEVELOPMENT_TOKEN": + DevelopmentToken = value; break; - case "DITTO_AUTH_URL": - AuthUrl = value; + case "DITTO_SERVER_URL": + ServerUrl = value; break; } } } // Validate required fields - if (string.IsNullOrWhiteSpace(AppId)) - throw new InvalidOperationException("DITTO_APP_ID is required in .env file"); - if (string.IsNullOrWhiteSpace(PlaygroundToken)) - throw new InvalidOperationException("DITTO_PLAYGROUND_TOKEN is required in .env file"); - if (string.IsNullOrWhiteSpace(AuthUrl)) - throw new InvalidOperationException("DITTO_AUTH_URL is required in .env file"); + if (string.IsNullOrWhiteSpace(DatabaseId)) + throw new InvalidOperationException("DITTO_DATABASE_ID is required in .env file"); + if (string.IsNullOrWhiteSpace(DevelopmentToken)) + throw new InvalidOperationException("DITTO_DEVELOPMENT_TOKEN is required in .env file"); + if (string.IsNullOrWhiteSpace(ServerUrl)) + throw new InvalidOperationException("DITTO_SERVER_URL is required in .env file"); } } } diff --git a/dotnet-winforms-net48/AppServices.cs b/dotnet-winforms-net48/AppServices.cs new file mode 100644 index 000000000..b839848e0 --- /dev/null +++ b/dotnet-winforms-net48/AppServices.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading.Tasks; + +namespace Taskapp.WinForms.Net48 +{ + /// + /// Singleton composition root that owns the and + /// and vends them for application-wide access. + /// + /// + /// This holder deliberately does NOT wrap the manager or repository with + /// pass-through methods. Callers use the exposed and + /// directly, keeping the quickstart free of an + /// abstraction layer over Ditto's API. + /// + public sealed class AppServices : IDisposable + { + private static readonly Lazy _instance = + new Lazy(() => new AppServices()); + + public static AppServices Instance => _instance.Value; + + private DittoManager _dittoManager; + private TasksRepository _tasksRepository; + private bool _isInitialized; + + private AppServices() + { + } + + /// + /// Initializes the Ditto instance and tasks repository with configuration values. + /// + public async Task InitializeAsync(string databaseId, string developmentToken, string serverUrl) + { + if (_isInitialized) + throw new InvalidOperationException("AppServices is already initialized"); + + _dittoManager = await DittoManager.Create(databaseId, developmentToken, serverUrl); + _tasksRepository = new TasksRepository(_dittoManager); + _isInitialized = true; + } + + /// + /// Gets whether the service has been initialized. + /// + public bool IsInitialized => _isInitialized; + + /// + /// The Ditto instance manager. Use it for instance/config/sync concerns. + /// + public DittoManager DittoManager + { + get + { + EnsureInitialized(); + return _dittoManager; + } + } + + /// + /// The tasks repository. Use it for subscription/observer/CRUD concerns. + /// + public TasksRepository TasksRepository + { + get + { + EnsureInitialized(); + return _tasksRepository; + } + } + + /// + /// Ensures the service has been initialized. + /// + private void EnsureInitialized() + { + if (!_isInitialized) + throw new InvalidOperationException("AppServices has not been initialized. Call InitializeAsync first."); + } + + public void Dispose() + { + _tasksRepository = null; + + if (_dittoManager != null) + { + _dittoManager.Dispose(); + _dittoManager = null; + } + + _isInitialized = false; + } + } +} diff --git a/dotnet-winforms-net48/DittoManager.cs b/dotnet-winforms-net48/DittoManager.cs new file mode 100644 index 000000000..5506d55cd --- /dev/null +++ b/dotnet-winforms-net48/DittoManager.cs @@ -0,0 +1,118 @@ +using System; +using System.Threading.Tasks; + +using DittoSDK; +using DittoSDK.Auth; + +namespace Taskapp.WinForms.Net48 +{ + /// + /// Manages the lifecycle and configuration of a Ditto instance: config, open, + /// auth, and starting/stopping sync. It vends a configured, already-running + /// instance (via the property) for the + /// tasks repository to use directly. + /// + /// + /// This class deliberately does NOT wrap Ditto's APIs (store/sync/etc.). The + /// repository calls the real Ditto API — e.g. dittoManager.Ditto.Store.ExecuteAsync(...) + /// — so the quickstart shows how to use Ditto directly rather than modeling an + /// abstraction layer over it. + /// + public class DittoManager : IDisposable + { + public string DatabaseId { get; private set; } + public string DevelopmentToken { get; private set; } + public string ServerUrl { get; private set; } + + public bool IsSyncActive => _ditto.Sync.IsActive; + + private readonly Ditto _ditto; + + /// + /// The configured, already-running Ditto instance. The tasks repository uses + /// this to call the real Ditto API directly. + /// + public Ditto Ditto => _ditto; + + /// + /// Creates a new synchronizing DittoManager instance. + /// + public static Task Create( + string databaseId, + string developmentToken, + string serverUrl) + { + var dittoManager = new DittoManager(databaseId, developmentToken, serverUrl); + dittoManager.Authenticate(); + dittoManager.StartSync(); + + return Task.FromResult(dittoManager); + } + + private void Authenticate() + { + _ditto.Auth.ExpirationHandler = async (ditto, secondsRemaining) => + { + // Authenticate when token is expiring + try + { + await ditto.Auth.LoginAsync( + // Your development token, replace with your actual token + DevelopmentToken, + // Use DittoAuthenticationProvider.Development, or your actual provider + DittoAuthenticationProvider.Development + ); + Console.WriteLine("Authentication successful"); + } + catch (Exception error) + { + Console.WriteLine($"Authentication failed: {error}"); + } + }; + } + + /// + /// Constructor + /// + /// Ditto database ID + /// Ditto development token + /// Ditto Server URL + private DittoManager(string databaseId, string developmentToken, string serverUrl) + { + DatabaseId = databaseId; + DevelopmentToken = developmentToken; + ServerUrl = serverUrl; + + var config = new DittoConfig( + DatabaseId, + new DittoConfigConnect.Server(new Uri(serverUrl)) + ); + + _ditto = Ditto.Open(config); + } + + public void Dispose() + { + _ditto.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// Start synchronizing with other peers and the Ditto server. + /// + public void StartSync() + { + _ditto.Sync.Start(); + } + + /// + /// Stop synchronizing. + /// + public void StopSync() + { + // Stop the sync engine only. The subscription is owned by TasksRepository + // and stays registered across a sync on/off toggle. + _ditto.Sync.Stop(); + } + } +} diff --git a/dotnet-winforms-net48/MainForm.Designer.cs b/dotnet-winforms-net48/MainForm.Designer.cs index 241f35ba8..98f66fab3 100644 --- a/dotnet-winforms-net48/MainForm.Designer.cs +++ b/dotnet-winforms-net48/MainForm.Designer.cs @@ -1,164 +1,164 @@ -namespace Taskapp.WinForms.Net48 -{ - partial class MainForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.tasksListView = new System.Windows.Forms.ListView(); - this.doneColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); - this.titleColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); - this.idColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); - this.newTaskTextBox = new System.Windows.Forms.TextBox(); - this.addTaskButton = new System.Windows.Forms.Button(); - this.editTaskButton = new System.Windows.Forms.Button(); - this.deleteTaskButton = new System.Windows.Forms.Button(); - this.statusLabel = new System.Windows.Forms.Label(); - this.SuspendLayout(); - // - // tasksListView - // - this.tasksListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.tasksListView.CheckBoxes = true; - this.tasksListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { - this.doneColumn, - this.titleColumn, - this.idColumn}); - this.tasksListView.FullRowSelect = true; - this.tasksListView.GridLines = true; - this.tasksListView.HideSelection = false; - this.tasksListView.Location = new System.Drawing.Point(12, 12); - this.tasksListView.Name = "tasksListView"; - this.tasksListView.Size = new System.Drawing.Size(760, 350); - this.tasksListView.TabIndex = 0; - this.tasksListView.UseCompatibleStateImageBehavior = false; - this.tasksListView.View = System.Windows.Forms.View.Details; - this.tasksListView.ItemChecked += new System.Windows.Forms.ItemCheckedEventHandler(this.tasksListView_ItemChecked); - // - // doneColumn - // - this.doneColumn.Text = "Done"; - this.doneColumn.Width = 50; - // - // titleColumn - // - this.titleColumn.Text = "Title"; - this.titleColumn.Width = 500; - // - // idColumn - // - this.idColumn.Text = "ID"; - this.idColumn.Width = 0; - // - // newTaskTextBox - // - this.newTaskTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.newTaskTextBox.Location = new System.Drawing.Point(12, 378); - this.newTaskTextBox.Name = "newTaskTextBox"; - this.newTaskTextBox.Size = new System.Drawing.Size(540, 20); - this.newTaskTextBox.TabIndex = 1; - this.newTaskTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.newTaskTextBox_KeyPress); - // - // addTaskButton - // - this.addTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.addTaskButton.Location = new System.Drawing.Point(558, 376); - this.addTaskButton.Name = "addTaskButton"; - this.addTaskButton.Size = new System.Drawing.Size(100, 23); - this.addTaskButton.TabIndex = 2; - this.addTaskButton.Text = "Add Task"; - this.addTaskButton.UseVisualStyleBackColor = true; - this.addTaskButton.Click += new System.EventHandler(this.addTaskButton_Click); - // - // editTaskButton - // - this.editTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.editTaskButton.Location = new System.Drawing.Point(664, 376); - this.editTaskButton.Name = "editTaskButton"; - this.editTaskButton.Size = new System.Drawing.Size(100, 23); - this.editTaskButton.TabIndex = 3; - this.editTaskButton.Text = "Edit Task"; - this.editTaskButton.UseVisualStyleBackColor = true; - this.editTaskButton.Click += new System.EventHandler(this.editTaskButton_Click); - // - // deleteTaskButton - // - this.deleteTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.deleteTaskButton.Location = new System.Drawing.Point(664, 405); - this.deleteTaskButton.Name = "deleteTaskButton"; - this.deleteTaskButton.Size = new System.Drawing.Size(100, 23); - this.deleteTaskButton.TabIndex = 4; - this.deleteTaskButton.Text = "Delete Task"; - this.deleteTaskButton.UseVisualStyleBackColor = true; - this.deleteTaskButton.Click += new System.EventHandler(this.deleteTaskButton_Click); - // - // statusLabel - // - this.statusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.statusLabel.AutoSize = true; - this.statusLabel.Location = new System.Drawing.Point(12, 410); - this.statusLabel.Name = "statusLabel"; - this.statusLabel.Size = new System.Drawing.Size(79, 13); - this.statusLabel.TabIndex = 5; - this.statusLabel.Text = "Status: Ready"; - // - // MainForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(784, 441); - this.Controls.Add(this.statusLabel); - this.Controls.Add(this.deleteTaskButton); - this.Controls.Add(this.editTaskButton); - this.Controls.Add(this.addTaskButton); - this.Controls.Add(this.newTaskTextBox); - this.Controls.Add(this.tasksListView); - this.Name = "MainForm"; - this.Text = "Tasks"; - this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); - this.Load += new System.EventHandler(this.MainForm_Load); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.ListView tasksListView; - private System.Windows.Forms.ColumnHeader doneColumn; - private System.Windows.Forms.ColumnHeader titleColumn; - private System.Windows.Forms.ColumnHeader idColumn; - private System.Windows.Forms.TextBox newTaskTextBox; - private System.Windows.Forms.Button addTaskButton; - private System.Windows.Forms.Button editTaskButton; - private System.Windows.Forms.Button deleteTaskButton; - private System.Windows.Forms.Label statusLabel; - } -} - +namespace Taskapp.WinForms.Net48 +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.tasksListView = new System.Windows.Forms.ListView(); + this.doneColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.titleColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.idColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.newTaskTextBox = new System.Windows.Forms.TextBox(); + this.addTaskButton = new System.Windows.Forms.Button(); + this.editTaskButton = new System.Windows.Forms.Button(); + this.deleteTaskButton = new System.Windows.Forms.Button(); + this.statusLabel = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // tasksListView + // + this.tasksListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.tasksListView.CheckBoxes = true; + this.tasksListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.doneColumn, + this.titleColumn, + this.idColumn}); + this.tasksListView.FullRowSelect = true; + this.tasksListView.GridLines = true; + this.tasksListView.HideSelection = false; + this.tasksListView.Location = new System.Drawing.Point(12, 12); + this.tasksListView.Name = "tasksListView"; + this.tasksListView.Size = new System.Drawing.Size(760, 350); + this.tasksListView.TabIndex = 0; + this.tasksListView.UseCompatibleStateImageBehavior = false; + this.tasksListView.View = System.Windows.Forms.View.Details; + this.tasksListView.ItemChecked += new System.Windows.Forms.ItemCheckedEventHandler(this.tasksListView_ItemChecked); + // + // doneColumn + // + this.doneColumn.Text = "Done"; + this.doneColumn.Width = 50; + // + // titleColumn + // + this.titleColumn.Text = "Title"; + this.titleColumn.Width = 500; + // + // idColumn + // + this.idColumn.Text = "ID"; + this.idColumn.Width = 0; + // + // newTaskTextBox + // + this.newTaskTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.newTaskTextBox.Location = new System.Drawing.Point(12, 378); + this.newTaskTextBox.Name = "newTaskTextBox"; + this.newTaskTextBox.Size = new System.Drawing.Size(540, 20); + this.newTaskTextBox.TabIndex = 1; + this.newTaskTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.newTaskTextBox_KeyPress); + // + // addTaskButton + // + this.addTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.addTaskButton.Location = new System.Drawing.Point(558, 376); + this.addTaskButton.Name = "addTaskButton"; + this.addTaskButton.Size = new System.Drawing.Size(100, 23); + this.addTaskButton.TabIndex = 2; + this.addTaskButton.Text = "Add Task"; + this.addTaskButton.UseVisualStyleBackColor = true; + this.addTaskButton.Click += new System.EventHandler(this.addTaskButton_Click); + // + // editTaskButton + // + this.editTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.editTaskButton.Location = new System.Drawing.Point(664, 376); + this.editTaskButton.Name = "editTaskButton"; + this.editTaskButton.Size = new System.Drawing.Size(100, 23); + this.editTaskButton.TabIndex = 3; + this.editTaskButton.Text = "Edit Task"; + this.editTaskButton.UseVisualStyleBackColor = true; + this.editTaskButton.Click += new System.EventHandler(this.editTaskButton_Click); + // + // deleteTaskButton + // + this.deleteTaskButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.deleteTaskButton.Location = new System.Drawing.Point(664, 405); + this.deleteTaskButton.Name = "deleteTaskButton"; + this.deleteTaskButton.Size = new System.Drawing.Size(100, 23); + this.deleteTaskButton.TabIndex = 4; + this.deleteTaskButton.Text = "Delete Task"; + this.deleteTaskButton.UseVisualStyleBackColor = true; + this.deleteTaskButton.Click += new System.EventHandler(this.deleteTaskButton_Click); + // + // statusLabel + // + this.statusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.statusLabel.AutoSize = true; + this.statusLabel.Location = new System.Drawing.Point(12, 410); + this.statusLabel.Name = "statusLabel"; + this.statusLabel.Size = new System.Drawing.Size(79, 13); + this.statusLabel.TabIndex = 5; + this.statusLabel.Text = "Status: Ready"; + // + // MainForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(784, 441); + this.Controls.Add(this.statusLabel); + this.Controls.Add(this.deleteTaskButton); + this.Controls.Add(this.editTaskButton); + this.Controls.Add(this.addTaskButton); + this.Controls.Add(this.newTaskTextBox); + this.Controls.Add(this.tasksListView); + this.Name = "MainForm"; + this.Text = "Tasks"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); + this.Load += new System.EventHandler(this.MainForm_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.ListView tasksListView; + private System.Windows.Forms.ColumnHeader doneColumn; + private System.Windows.Forms.ColumnHeader titleColumn; + private System.Windows.Forms.ColumnHeader idColumn; + private System.Windows.Forms.TextBox newTaskTextBox; + private System.Windows.Forms.Button addTaskButton; + private System.Windows.Forms.Button editTaskButton; + private System.Windows.Forms.Button deleteTaskButton; + private System.Windows.Forms.Label statusLabel; + } +} + diff --git a/dotnet-winforms-net48/MainForm.cs b/dotnet-winforms-net48/MainForm.cs index a69a70339..df2d8adf9 100644 --- a/dotnet-winforms-net48/MainForm.cs +++ b/dotnet-winforms-net48/MainForm.cs @@ -1,253 +1,260 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace Taskapp.WinForms.Net48 -{ - public partial class MainForm : Form - { - private bool _isUpdatingListView = false; - - public MainForm() - { - InitializeComponent(); - } - - private void MainForm_Load(object sender, EventArgs e) - { - // Enable double buffering to reduce flicker - typeof(ListView).InvokeMember("DoubleBuffered", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty, - null, tasksListView, new object[] { true }); - - // Register observer to watch for task changes - TasksPeerService.Instance.ObserveTasksCollection(OnTasksChanged); - - // Update status - UpdateSyncStatus(); - } - - private async Task OnTasksChanged(IList tasks) - { - // Invoke on UI thread - if (InvokeRequired) - { - Invoke(new Action(() => UpdateTasksList(tasks))); - } - else - { - UpdateTasksList(tasks); - } - - await Task.CompletedTask; - } - - private void UpdateTasksList(IList tasks) - { - _isUpdatingListView = true; - tasksListView.BeginUpdate(); - - try - { - // Filter out null tasks and tasks with null/empty Id - var validTasks = tasks - .Where(t => t != null && !string.IsNullOrEmpty(t.Id)) - .ToList(); - - // Create dictionary of current items by ID for quick lookup - var currentItems = new Dictionary(); - foreach (ListViewItem item in tasksListView.Items) - { - var id = item.SubItems[2].Text; // ID is in third column - currentItems[id] = item; - } - - // Create dictionary of incoming tasks by ID - var incomingTasks = validTasks.ToDictionary(t => t.Id); - - // Update existing items and add new ones - foreach (var task in validTasks) - { - if (currentItems.ContainsKey(task.Id)) - { - // Update existing item - var item = currentItems[task.Id]; - item.SubItems[1].Text = task.Title; - item.Checked = task.Done; - } - else - { - // Add new item - var item = new ListViewItem(); - item.Checked = task.Done; - item.SubItems.Add(task.Title); - item.SubItems.Add(task.Id); - tasksListView.Items.Add(item); - } - } - - // Remove items that are no longer in the incoming list - var itemsToRemove = new List(); - foreach (ListViewItem item in tasksListView.Items) - { - var id = item.SubItems[2].Text; - if (!incomingTasks.ContainsKey(id)) - { - itemsToRemove.Add(item); - } - } - - foreach (var item in itemsToRemove) - { - tasksListView.Items.Remove(item); - } - } - finally - { - tasksListView.EndUpdate(); - _isUpdatingListView = false; - } - } - - private async void addTaskButton_Click(object sender, EventArgs e) - { - var title = newTaskTextBox.Text.Trim(); - if (string.IsNullOrWhiteSpace(title)) - { - MessageBox.Show("Please enter a task title.", "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - - try - { - await TasksPeerService.Instance.AddTaskAsync(title); - newTaskTextBox.Clear(); - UpdateStatus("Task added successfully"); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to add task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - - private async void editTaskButton_Click(object sender, EventArgs e) - { - if (tasksListView.SelectedItems.Count == 0) - { - MessageBox.Show("Please select a task to edit.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - var selectedItem = tasksListView.SelectedItems[0]; - var taskId = selectedItem.SubItems[2].Text; - var currentTitle = selectedItem.SubItems[1].Text; - - var newTitle = InputDialog.Show("Enter new title:", "Edit Task", currentTitle); - if (string.IsNullOrWhiteSpace(newTitle) || newTitle == currentTitle) - { - return; - } - - try - { - await TasksPeerService.Instance.UpdateTaskTitleAsync(taskId, newTitle); - UpdateStatus("Task updated successfully"); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to update task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - - private async void deleteTaskButton_Click(object sender, EventArgs e) - { - if (tasksListView.SelectedItems.Count == 0) - { - MessageBox.Show("Please select a task to delete.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - var selectedItem = tasksListView.SelectedItems[0]; - var taskTitle = selectedItem.SubItems[1].Text; - var taskId = selectedItem.SubItems[2].Text; - - var result = MessageBox.Show( - $"Are you sure you want to delete the task '{taskTitle}'?", - "Confirm Delete", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question - ); - - if (result == DialogResult.Yes) - { - try - { - await TasksPeerService.Instance.DeleteTaskAsync(taskId); - UpdateStatus("Task deleted successfully"); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to delete task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - } - - private async void tasksListView_ItemChecked(object sender, ItemCheckedEventArgs e) - { - // Prevent recursive updates (including the revert flip below) - if (_isUpdatingListView) - return; - - var item = e.Item; - var taskId = item.SubItems[2].Text; - var newCheckedState = item.Checked; - - try - { - await TasksPeerService.Instance.UpdateTaskDoneAsync(taskId, newCheckedState); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to update task status: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - // Revert the check state; guard prevents this flip from re-entering - _isUpdatingListView = true; - item.Checked = !newCheckedState; - _isUpdatingListView = false; - } - } - - private void newTaskTextBox_KeyPress(object sender, KeyPressEventArgs e) - { - // Allow adding task with Enter key - if (e.KeyChar == (char)Keys.Enter) - { - e.Handled = true; - addTaskButton_Click(sender, EventArgs.Empty); - } - } - - private void UpdateStatus(string message) - { - statusLabel.Text = $"Status: {message}"; - } - - private void UpdateSyncStatus() - { - if (TasksPeerService.Instance.IsSyncActive) - { - statusLabel.Text = "Status: Syncing"; - } - else - { - statusLabel.Text = "Status: Ready"; - } - } - - private void MainForm_FormClosing(object sender, FormClosingEventArgs e) - { - // Cleanup handled in Program.cs - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using DittoSDK.Store; + +namespace Taskapp.WinForms.Net48 +{ + public partial class MainForm : Form + { + private bool _isUpdatingListView = false; + private DittoStoreObserver _tasksObserver; + + public MainForm() + { + InitializeComponent(); + } + + private void MainForm_Load(object sender, EventArgs e) + { + // Enable double buffering to reduce flicker + typeof(ListView).InvokeMember("DoubleBuffered", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty, + null, tasksListView, new object[] { true }); + + // Register observer to watch for task changes + _tasksObserver = AppServices.Instance.TasksRepository.ObserveTasksCollection(OnTasksChanged); + + // Update status + UpdateSyncStatus(); + } + + private async Task OnTasksChanged(IList tasks) + { + // Invoke on UI thread + if (InvokeRequired) + { + Invoke(new Action(() => UpdateTasksList(tasks))); + } + else + { + UpdateTasksList(tasks); + } + + await Task.CompletedTask; + } + + private void UpdateTasksList(IList tasks) + { + _isUpdatingListView = true; + tasksListView.BeginUpdate(); + + try + { + // Filter out null tasks and tasks with null/empty Id + var validTasks = tasks + .Where(t => t != null && !string.IsNullOrEmpty(t.Id)) + .ToList(); + + // Create dictionary of current items by ID for quick lookup + var currentItems = new Dictionary(); + foreach (ListViewItem item in tasksListView.Items) + { + var id = item.SubItems[2].Text; // ID is in third column + currentItems[id] = item; + } + + // Create dictionary of incoming tasks by ID + var incomingTasks = validTasks.ToDictionary(t => t.Id); + + // Update existing items and add new ones + foreach (var task in validTasks) + { + if (currentItems.ContainsKey(task.Id)) + { + // Update existing item + var item = currentItems[task.Id]; + item.SubItems[1].Text = task.Title; + item.Checked = task.Done; + } + else + { + // Add new item + var item = new ListViewItem(); + item.Checked = task.Done; + item.SubItems.Add(task.Title); + item.SubItems.Add(task.Id); + tasksListView.Items.Add(item); + } + } + + // Remove items that are no longer in the incoming list + var itemsToRemove = new List(); + foreach (ListViewItem item in tasksListView.Items) + { + var id = item.SubItems[2].Text; + if (!incomingTasks.ContainsKey(id)) + { + itemsToRemove.Add(item); + } + } + + foreach (var item in itemsToRemove) + { + tasksListView.Items.Remove(item); + } + } + finally + { + tasksListView.EndUpdate(); + _isUpdatingListView = false; + } + } + + private async void addTaskButton_Click(object sender, EventArgs e) + { + var title = newTaskTextBox.Text.Trim(); + if (string.IsNullOrWhiteSpace(title)) + { + MessageBox.Show("Please enter a task title.", "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + try + { + await AppServices.Instance.TasksRepository.AddTask(title); + newTaskTextBox.Clear(); + UpdateStatus("Task added successfully"); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to add task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private async void editTaskButton_Click(object sender, EventArgs e) + { + if (tasksListView.SelectedItems.Count == 0) + { + MessageBox.Show("Please select a task to edit.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var selectedItem = tasksListView.SelectedItems[0]; + var taskId = selectedItem.SubItems[2].Text; + var currentTitle = selectedItem.SubItems[1].Text; + + var newTitle = InputDialog.Show("Enter new title:", "Edit Task", currentTitle); + if (string.IsNullOrWhiteSpace(newTitle) || newTitle == currentTitle) + { + return; + } + + try + { + await AppServices.Instance.TasksRepository.UpdateTaskTitle(taskId, newTitle); + UpdateStatus("Task updated successfully"); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to update task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private async void deleteTaskButton_Click(object sender, EventArgs e) + { + if (tasksListView.SelectedItems.Count == 0) + { + MessageBox.Show("Please select a task to delete.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var selectedItem = tasksListView.SelectedItems[0]; + var taskTitle = selectedItem.SubItems[1].Text; + var taskId = selectedItem.SubItems[2].Text; + + var result = MessageBox.Show( + $"Are you sure you want to delete the task '{taskTitle}'?", + "Confirm Delete", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question + ); + + if (result == DialogResult.Yes) + { + try + { + await AppServices.Instance.TasksRepository.DeleteTask(taskId); + UpdateStatus("Task deleted successfully"); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to delete task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private async void tasksListView_ItemChecked(object sender, ItemCheckedEventArgs e) + { + // Prevent recursive updates (including the revert flip below) + if (_isUpdatingListView) + return; + + var item = e.Item; + var taskId = item.SubItems[2].Text; + var newCheckedState = item.Checked; + + try + { + await AppServices.Instance.TasksRepository.UpdateTaskDone(taskId, newCheckedState); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to update task status: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + // Revert the check state; guard prevents this flip from re-entering + _isUpdatingListView = true; + item.Checked = !newCheckedState; + _isUpdatingListView = false; + } + } + + private void newTaskTextBox_KeyPress(object sender, KeyPressEventArgs e) + { + // Allow adding task with Enter key + if (e.KeyChar == (char)Keys.Enter) + { + e.Handled = true; + addTaskButton_Click(sender, EventArgs.Empty); + } + } + + private void UpdateStatus(string message) + { + statusLabel.Text = $"Status: {message}"; + } + + private void UpdateSyncStatus() + { + if (AppServices.Instance.DittoManager.IsSyncActive) + { + statusLabel.Text = "Status: Syncing"; + } + else + { + statusLabel.Text = "Status: Ready"; + } + } + + private void MainForm_FormClosing(object sender, FormClosingEventArgs e) + { + // Stop observing; the Ditto instance itself is disposed in Program.cs + if (_tasksObserver != null) + { + _tasksObserver.Cancel(); + _tasksObserver = null; + } + } + } +} diff --git a/dotnet-winforms-net48/MainForm.resx b/dotnet-winforms-net48/MainForm.resx index 29dcb1b3a..1af7de150 100644 --- a/dotnet-winforms-net48/MainForm.resx +++ b/dotnet-winforms-net48/MainForm.resx @@ -1,120 +1,120 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/dotnet-winforms-net48/Program.cs b/dotnet-winforms-net48/Program.cs index e5b69f174..9de449348 100644 --- a/dotnet-winforms-net48/Program.cs +++ b/dotnet-winforms-net48/Program.cs @@ -1,68 +1,68 @@ -using System; -using System.Windows.Forms; - -namespace Taskapp.WinForms.Net48 -{ - internal static class Program - { - /// - /// The main entry point for the application. - /// - [STAThread] - static void Main() - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - - try - { - // Load configuration from .env file - AppConfiguration.Load(); - - // Initialize TasksPeerService asynchronously - var initTask = TasksPeerService.Instance.InitializeAsync( - AppConfiguration.AppId, - AppConfiguration.PlaygroundToken, - AppConfiguration.AuthUrl - ); - - // Show loading form while initializing - using (var loadingForm = new LoadingForm(initTask)) - { - if (loadingForm.ShowDialog() == DialogResult.OK) - { - // Initialization successful, show main form - Application.Run(new MainForm()); - } - else - { - // Initialization failed or was cancelled - MessageBox.Show( - "Failed to initialize Ditto. Please check your configuration and try again.", - "Initialization Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ); - } - } - } - catch (Exception ex) - { - MessageBox.Show( - $"Application startup failed: {ex.Message}", - "Startup Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ); - } - finally - { - // Cleanup on exit - if (TasksPeerService.Instance.IsInitialized) - { - TasksPeerService.Instance.Dispose(); - } - } - } - } -} +using System; +using System.Windows.Forms; + +namespace Taskapp.WinForms.Net48 +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + try + { + // Load configuration from .env file + AppConfiguration.Load(); + + // Initialize AppServices asynchronously + var initTask = AppServices.Instance.InitializeAsync( + AppConfiguration.DatabaseId, + AppConfiguration.DevelopmentToken, + AppConfiguration.ServerUrl + ); + + // Show loading form while initializing + using (var loadingForm = new LoadingForm(initTask)) + { + if (loadingForm.ShowDialog() == DialogResult.OK) + { + // Initialization successful, show main form + Application.Run(new MainForm()); + } + else + { + // Initialization failed or was cancelled + MessageBox.Show( + "Failed to initialize Ditto. Please check your configuration and try again.", + "Initialization Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + } + } + catch (Exception ex) + { + MessageBox.Show( + $"Application startup failed: {ex.Message}", + "Startup Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + finally + { + // Cleanup on exit + if (AppServices.Instance.IsInitialized) + { + AppServices.Instance.Dispose(); + } + } + } + } +} diff --git a/dotnet-winforms-net48/Properties/AssemblyInfo.cs b/dotnet-winforms-net48/Properties/AssemblyInfo.cs index 94f3c000b..e4fb49ca4 100644 --- a/dotnet-winforms-net48/Properties/AssemblyInfo.cs +++ b/dotnet-winforms-net48/Properties/AssemblyInfo.cs @@ -1,33 +1,33 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Taskapp.WinForms.Net48")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Ditto")] -[assembly: AssemblyProduct("Taskapp.WinForms.Net48")] -[assembly: AssemblyCopyright("Copyright Ā© Ditto Live 2026")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("9f61044a-836b-42c3-974e-1cca09ee86a2")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Taskapp.WinForms.Net48")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Ditto")] +[assembly: AssemblyProduct("Taskapp.WinForms.Net48")] +[assembly: AssemblyCopyright("Copyright Ā© Ditto Live 2026")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9f61044a-836b-42c3-974e-1cca09ee86a2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/dotnet-winforms-net48/Properties/Resources.Designer.cs b/dotnet-winforms-net48/Properties/Resources.Designer.cs index c6bdade56..dc069845d 100644 --- a/dotnet-winforms-net48/Properties/Resources.Designer.cs +++ b/dotnet-winforms-net48/Properties/Resources.Designer.cs @@ -1,71 +1,71 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Taskapp.WinForms.Net48.Properties -{ - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Taskapp.WinForms.Net48.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { - return resourceCulture; - } - set - { - resourceCulture = value; - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Taskapp.WinForms.Net48.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Taskapp.WinForms.Net48.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/dotnet-winforms-net48/Properties/Resources.resx b/dotnet-winforms-net48/Properties/Resources.resx index ffecec851..af7dbebba 100644 --- a/dotnet-winforms-net48/Properties/Resources.resx +++ b/dotnet-winforms-net48/Properties/Resources.resx @@ -1,117 +1,117 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/dotnet-winforms-net48/Properties/Settings.Designer.cs b/dotnet-winforms-net48/Properties/Settings.Designer.cs index 28a61c00a..761f2f69a 100644 --- a/dotnet-winforms-net48/Properties/Settings.Designer.cs +++ b/dotnet-winforms-net48/Properties/Settings.Designer.cs @@ -1,30 +1,30 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Taskapp.WinForms.Net48.Properties -{ - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { - return defaultInstance; - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Taskapp.WinForms.Net48.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/dotnet-winforms-net48/Properties/Settings.settings b/dotnet-winforms-net48/Properties/Settings.settings index abf36c5d3..39645652a 100644 --- a/dotnet-winforms-net48/Properties/Settings.settings +++ b/dotnet-winforms-net48/Properties/Settings.settings @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/dotnet-winforms-net48/README.md b/dotnet-winforms-net48/README.md index 999744a3f..b0d7b39f8 100644 --- a/dotnet-winforms-net48/README.md +++ b/dotnet-winforms-net48/README.md @@ -7,14 +7,14 @@ - Does not support ARM64 (however, it can run on Windows on ARM using x64 emulation if supported by the OS/hardware). ARM64 is not officially supported by Ditto for use with the .NET 4.8 framework. 1. Install the .NET 4.8 SDK from -2. Create an application at . Make note of the app ID and online playground token -3. Copy `.env.sample` from the repo root into the `dotnet-winforms-net48/` folder, rename the copy to `.env`, and fill in your `DITTO_APP_ID`, `DITTO_PLAYGROUND_TOKEN`, and `DITTO_AUTH_URL` (from ). The `.env` file must live next to `Taskapp.WinForms.Net48.csproj` — this app reads its config from the project folder, not the repo root. (`DITTO_WEBSOCKET_URL` is in the sample but unused by this app; leave it blank.) +2. Create an application at . Make note of the Database ID and development token +3. Copy `.env.sample` from the repo root into the `dotnet-winforms-net48/` folder, rename the copy to `.env`, and fill in your `DITTO_DATABASE_ID`, `DITTO_DEVELOPMENT_TOKEN`, and `DITTO_SERVER_URL` (from ). The `.env` file must live next to `Taskapp.WinForms.Net48.csproj` — this app reads its config from the project folder, not the repo root. ## Documentation - [Ditto C# .NET SDK Install Guide](https://docs.ditto.live/install-guides/c-sharp) -- [Ditto C# .NET SDK API Reference](https://software.ditto.live/dotnet/Ditto/5.0.1/api-reference/) +- [Ditto C# .NET SDK API Reference](https://docs.ditto.live/sdk/latest/api-reference/c-sharp) ## .NET Windows Forms Application diff --git a/dotnet-winforms-net48/ToDoTask.cs b/dotnet-winforms-net48/TaskModel.cs similarity index 83% rename from dotnet-winforms-net48/ToDoTask.cs rename to dotnet-winforms-net48/TaskModel.cs index 109c553a4..97a72b4fb 100644 --- a/dotnet-winforms-net48/ToDoTask.cs +++ b/dotnet-winforms-net48/TaskModel.cs @@ -1,29 +1,29 @@ -using Newtonsoft.Json; -using System; - -namespace Taskapp.WinForms.Net48 -{ - /// - /// Representation of a document in the Ditto 'tasks' collection. - /// - /// - /// This class is named ToDoTask rather than Task to avoid - /// conflicts with the System.Threading.Tasks.Task class. - /// - public class ToDoTask - { - [JsonProperty("_id")] - public string Id { get; set; } - - [JsonProperty("title")] - public string Title { get; set; } - - [JsonProperty("done")] - public bool Done { get; set; } - - [JsonProperty("deleted")] - public bool Deleted { get; set; } - - override public string ToString() => Title; - } -} +using Newtonsoft.Json; +using System; + +namespace Taskapp.WinForms.Net48 +{ + /// + /// Representation of a document in the Ditto 'tasks' collection. + /// + /// + /// This class is named TaskModel rather than Task to avoid + /// conflicts with the System.Threading.Tasks.Task class. + /// + public class TaskModel + { + [JsonProperty("_id")] + public string Id { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("done")] + public bool Done { get; set; } + + [JsonProperty("deleted")] + public bool Deleted { get; set; } + + override public string ToString() => Title; + } +} diff --git a/dotnet-winforms-net48/Taskapp.WinForms.Net48.csproj b/dotnet-winforms-net48/Taskapp.WinForms.Net48.csproj index 7f60ecbc5..15784791d 100644 --- a/dotnet-winforms-net48/Taskapp.WinForms.Net48.csproj +++ b/dotnet-winforms-net48/Taskapp.WinForms.Net48.csproj @@ -1,161 +1,161 @@ - - - - - Debug - AnyCPU - {9F61044A-836B-42C3-974E-1CCA09EE86A2} - WinExe - Taskapp.WinForms.Net48 - Taskapp.WinForms.Net48 - v4.8 - 512 - true - true - - - x64 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - x64 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - packages\PeterO.Cbor.4.5.2\lib\net40\CBOR.dll - - - packages\Ditto.5.0.1\lib\netstandard2.0\Ditto.dll - - - packages\Ditto.Native.Linux.5.0.1\lib\netstandard2.0\Ditto.Native.Linux.dll - - - packages\Ditto.Native.MacOS.5.0.1\lib\netstandard2.0\Ditto.Native.MacOS.dll - - - packages\Ditto.Native.Win.5.0.1\lib\netstandard2.0\Ditto.Native.Win.dll - - - packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll - - - packages\Microsoft.Bcl.HashCode.1.1.1\lib\net461\Microsoft.Bcl.HashCode.dll - - - packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll - - - packages\PeterO.Numbers.1.8.2\lib\net40\Numbers.dll - - - - packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - - packages\System.Memory.4.5.5\lib\net461\System.Memory.dll - - - - packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - - packages\System.Text.Encodings.Web.8.0.0\lib\net462\System.Text.Encodings.Web.dll - - - packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll - - - packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - - - packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - - - - - - - - - - - - packages\PeterO.URIUtility.1.0.0\lib\net40\URIUtility.dll - - - - - - - Form - - - LoadingForm.cs - - - Form - - - MainForm.cs - - - - - - - - MainForm.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - PreserveNewest - - - - - dittoffi.dll - PreserveNewest - - - + + + + + Debug + AnyCPU + {9F61044A-836B-42C3-974E-1CCA09EE86A2} + WinExe + Taskapp.WinForms.Net48 + Taskapp.WinForms.Net48 + v4.8 + 512 + true + true + + + x64 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + x64 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + packages\PeterO.Cbor.4.5.2\lib\net40\CBOR.dll + + + + packages\Ditto.5.0.3\lib\netstandard2.0\Ditto.dll + False + + + packages\Ditto.Native.Linux.5.0.3\lib\netstandard2.0\Ditto.Native.Linux.dll + + + packages\Ditto.Native.MacOS.5.0.3\lib\netstandard2.0\Ditto.Native.MacOS.dll + + + packages\Ditto.Native.Win.5.0.3\lib\netstandard2.0\Ditto.Native.Win.dll + + + packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + + packages\Microsoft.Bcl.HashCode.1.1.1\lib\net461\Microsoft.Bcl.HashCode.dll + + + packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll + + + packages\PeterO.Numbers.1.8.2\lib\net40\Numbers.dll + + + + packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + + packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + + packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + packages\System.Text.Encodings.Web.8.0.0\lib\net462\System.Text.Encodings.Web.dll + + + packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll + + + packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + + + + + + + + + + + + packages\PeterO.URIUtility.1.0.0\lib\net40\URIUtility.dll + + + + + + + Form + + + LoadingForm.cs + + + Form + + + MainForm.cs + + + + + + + + + MainForm.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/dotnet-winforms-net48/Taskapp.WinForms.Net48.sln b/dotnet-winforms-net48/Taskapp.WinForms.Net48.sln index d9d5c4ad9..782f61171 100644 --- a/dotnet-winforms-net48/Taskapp.WinForms.Net48.sln +++ b/dotnet-winforms-net48/Taskapp.WinForms.Net48.sln @@ -1,25 +1,25 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28729.10 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskapp.WinForms.Net48", "Taskapp.WinForms.Net48.csproj", "{9F61044A-836B-42C3-974E-1CCA09EE86A2}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {FEC06644-F3C3-4034-843B-D871F0B794C8} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28729.10 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Taskapp.WinForms.Net48", "Taskapp.WinForms.Net48.csproj", "{9F61044A-836B-42C3-974E-1CCA09EE86A2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F61044A-836B-42C3-974E-1CCA09EE86A2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FEC06644-F3C3-4034-843B-D871F0B794C8} + EndGlobalSection +EndGlobal diff --git a/dotnet-winforms-net48/TasksPeer.cs b/dotnet-winforms-net48/TasksPeer.cs deleted file mode 100644 index 4d57b06f9..000000000 --- a/dotnet-winforms-net48/TasksPeer.cs +++ /dev/null @@ -1,290 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -using DittoSDK; -using DittoSDK.Auth; -using DittoSDK.Store; -using Newtonsoft.Json; - -/// -/// Encapsulates use of the Ditto SDK and the 'tasks' collection. -/// - -namespace Taskapp.WinForms.Net48 -{ - public class TasksPeer : IDisposable - { - private const string Query = "SELECT * FROM tasks WHERE NOT deleted"; - - public string AppId { get; private set; } - public string PlaygroundToken { get; private set; } - public string AuthUrl { get; private set; } - - public bool IsSyncActive => _ditto.Sync.IsActive; - - private readonly Ditto _ditto; - - /// - /// Creates a new synchronizing TasksPeer instance. - /// - public static async Task Create( - string appId, - string playgroundToken, - string authUrl) - { - var peer = new TasksPeer(appId, playgroundToken, authUrl); - peer.Authenticate(); - peer.RegisterSubscription(); - await peer.InsertInitialTasks(); - peer.StartSync(); - - return peer; - } - - private void Authenticate() - { - _ditto.Auth.ExpirationHandler = async (ditto, secondsRemaining) => - { - // Authenticate when token is expiring - try - { - await ditto.Auth.LoginAsync( - // Your development token, replace with your actual token - PlaygroundToken, - // Use DittoAuthenticationProvider.Development for playground, or your actual provider - DittoAuthenticationProvider.Development - ); - Console.WriteLine("Authentication successful"); - } - catch (Exception error) - { - Console.WriteLine($"Authentication failed: {error}"); - } - }; - } - - /// - /// Registers a subscription for the tasks collection to enable data synchronization - /// with other peers and the Ditto cloud. This subscription determines what data - /// will be synced to this peer during the synchronization process. - /// - /// - /// The subscription is created using the same query that filters out deleted tasks - /// (SELECT * FROM tasks WHERE NOT deleted), ensuring that only active, - /// non-deleted tasks are synchronized across the network. - /// - /// This method should be called during peer initialization to establish the - /// subscription before starting the sync process. The subscription remains active - /// until explicitly cancelled or when the peer is disposed. - /// - /// - private void RegisterSubscription() - { - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - _ditto.Sync.RegisterSubscription(Query); - } - - /// - /// Constructor - /// - /// Ditto application ID - /// Ditto online playground token - /// Ditto Auth URL - private TasksPeer(string appId, string playgroundToken, string authUrl) - { - AppId = appId; - PlaygroundToken = playgroundToken; - AuthUrl = authUrl; - - var config = new DittoConfig( - AppId, - new DittoConfigConnect.Server(new Uri(authUrl)) - ); - - _ditto = Ditto.Open(config); - } - - public void Dispose() - { - _ditto.Dispose(); - GC.SuppressFinalize(this); - } - - /// - /// Inserts the initial tasks into the 'tasks' collection. - /// - private async Task InsertInitialTasks() - { - var initialTasks = new List> - { - new Dictionary - { - {"_id", "50191411-4C46-4940-8B72-5F8017A04FA7"}, - {"title", "Buy groceries"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "6DA283DA-8CFE-4526-A6FA-D385089364E5"}, - {"title", "Clean the kitchen"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "5303DDF8-0E72-4FEB-9E82-4B007E5797F0"}, - {"title", "Schedule dentist appointment"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "38411F1B-6B49-4346-90C3-0B16CE97E174"}, - {"title", "Pay bills"}, - {"done", false}, - {"deleted", false} - } - }; - - const string insertCommand = "INSERT INTO tasks INITIAL DOCUMENTS (:task)"; - foreach (var task in initialTasks) - { - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "task", task } - }); - } - } - - /// - /// Adds a new task to the 'tasks' collection. - /// - public async Task AddTask(string title) - { - if (string.IsNullOrWhiteSpace(title)) - { - throw new ArgumentException("title cannot be empty"); - } - - var doc = new Dictionary - { - {"title", title}, - {"done", false}, - {"deleted", false } - }; - const string insertCommand = "INSERT INTO tasks DOCUMENTS (:doc)"; - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "doc", doc } - }); - } - - /// - /// Update the title of an existing task. - /// - public async Task UpdateTaskTitle(string taskId, string newTitle) - { - if (string.IsNullOrWhiteSpace(taskId)) - { - throw new ArgumentException("taskId cannot be empty"); - } - - if (string.IsNullOrWhiteSpace(newTitle)) - { - throw new ArgumentException("title cannot be empty"); - } - - const string updateQuery = "UPDATE tasks SET title = :title WHERE _id = :id"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - {"title", newTitle}, - {"id", taskId} - }); - } - - /// - /// Mark a task as deleted. - /// - public async Task DeleteTask(string taskId) - { - if (string.IsNullOrWhiteSpace(taskId)) - { - throw new ArgumentException("taskId cannot be empty"); - } - - const string updateQuery = "UPDATE tasks SET deleted = true WHERE _id = :id"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - { "id", taskId } - }); - } - - /// - /// Mark a task as complete or not complete. - /// - public async Task UpdateTaskDone(string taskId, bool newDoneState) - { - const string updateQuery = "UPDATE tasks SET done = :newDoneState WHERE _id = :id AND done != :newDoneState"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary - { - { "newDoneState", newDoneState }, - { "id", taskId } - }); - } - - /// - /// Specify a handler to be called asynchronously when the task collection changes. - /// - public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) - { - // Register an observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - return _ditto.Store.RegisterObserver(Query, async (queryResult) => - { - try - { - // Deserialize the JSON documents into ToDoTask objects - var tasks = queryResult.Items.Select(d => - { - using (var reader = new StringReader(d.JsonString())) - using (var jsonReader = new JsonTextReader(reader)) - { - return JsonSerializer.Create().Deserialize(jsonReader); - } - }).OrderBy(t => t?.Id).ToList(); - - await handler(tasks); - } - catch (Exception e) - { - Console.Error.WriteLine($"ERROR tasks observation handler failed: {e.Message}"); - } - }); - } - - /// - /// Start synchronizing the 'tasks' collection. - /// - public void StartSync() - { - _ditto.Sync.Start(); - } - - /// - /// Stop synchronizing the 'tasks' collection. - /// - public void StopSync() - { - foreach (var subscription in _ditto.Sync.Subscriptions) - { - subscription.Cancel(); - } - _ditto.Sync.Stop(); - } - } -} diff --git a/dotnet-winforms-net48/TasksPeerService.cs b/dotnet-winforms-net48/TasksPeerService.cs deleted file mode 100644 index 628ca94dd..000000000 --- a/dotnet-winforms-net48/TasksPeerService.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DittoSDK; -using DittoSDK.Store; - -namespace Taskapp.WinForms.Net48 -{ - /// - /// Singleton service that wraps TasksPeer instance and provides application-wide access - /// - public sealed class TasksPeerService : IDisposable - { - private static readonly Lazy _instance = - new Lazy(() => new TasksPeerService()); - - public static TasksPeerService Instance => _instance.Value; - - private TasksPeer _tasksPeer; - private DittoStoreObserver _observer; - private bool _isInitialized; - - private TasksPeerService() - { - } - - /// - /// Initializes the TasksPeer with configuration values - /// - public async Task InitializeAsync(string appId, string playgroundToken, string authUrl) - { - if (_isInitialized) - throw new InvalidOperationException("TasksPeerService is already initialized"); - - _tasksPeer = await TasksPeer.Create(appId, playgroundToken, authUrl); - _isInitialized = true; - } - - /// - /// Gets whether the service has been initialized - /// - public bool IsInitialized => _isInitialized; - - /// - /// Gets whether sync is active - /// - public bool IsSyncActive => _tasksPeer?.IsSyncActive ?? false; - - /// - /// Adds a new task - /// - public async Task AddTaskAsync(string title) - { - EnsureInitialized(); - await _tasksPeer.AddTask(title); - } - - /// - /// Updates a task's title - /// - public async Task UpdateTaskTitleAsync(string taskId, string newTitle) - { - EnsureInitialized(); - await _tasksPeer.UpdateTaskTitle(taskId, newTitle); - } - - /// - /// Marks a task as deleted - /// - public async Task DeleteTaskAsync(string taskId) - { - EnsureInitialized(); - await _tasksPeer.DeleteTask(taskId); - } - - /// - /// Updates a task's done status - /// - public async Task UpdateTaskDoneAsync(string taskId, bool isDone) - { - EnsureInitialized(); - await _tasksPeer.UpdateTaskDone(taskId, isDone); - } - - /// - /// Registers an observer to watch for task collection changes - /// - public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) - { - EnsureInitialized(); - - // Cancel any existing observer before registering a new one - if (_observer != null) - { - _observer.Cancel(); - _observer = null; - } - _observer = _tasksPeer.ObserveTasksCollection(handler); - return _observer; - } - - /// - /// Ensures the service has been initialized - /// - private void EnsureInitialized() - { - if (!_isInitialized) - throw new InvalidOperationException("TasksPeerService has not been initialized. Call InitializeAsync first."); - } - - public void Dispose() - { - if (_observer != null) - { - _observer.Cancel(); - _observer = null; - } - - if (_tasksPeer != null) - { - _tasksPeer.Dispose(); - _tasksPeer = null; - } - - _isInitialized = false; - } - } -} diff --git a/dotnet-winforms-net48/TasksRepository.cs b/dotnet-winforms-net48/TasksRepository.cs new file mode 100644 index 000000000..f2003efc5 --- /dev/null +++ b/dotnet-winforms-net48/TasksRepository.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using DittoSDK.Store; +using Newtonsoft.Json; + +namespace Taskapp.WinForms.Net48 +{ + /// + /// Owns all 'tasks' collection data operations: the DQL query strings, the sync + /// subscription, the store observer, and CRUD. It calls the real Ditto API + /// directly through the it is given. + /// + public class TasksRepository + { + // The subscription controls what syncs to this device and is written to + // the local database; the observer reacts to changes in that local + // database, hiding soft-deleted tasks from the displayed list. + private const string SubscriptionQuery = "SELECT * FROM tasks"; + private const string ObserverQuery = "SELECT * FROM tasks WHERE NOT deleted"; + + private readonly DittoManager _dittoManager; + + public TasksRepository(DittoManager dittoManager) + { + _dittoManager = dittoManager; + RegisterSubscription(); + } + + /// + /// Registers a subscription for the tasks collection to enable data synchronization + /// with other peers and the Ditto server. This subscription determines what data + /// will be synced to this device during the synchronization process. + /// + /// + /// The subscription syncs all tasks (SELECT * FROM tasks) and controls + /// what is written to the local database; the observer query hides + /// soft-deleted tasks from the displayed list. + /// + /// + private void RegisterSubscription() + { + // Register a subscription, which determines what data syncs to this device + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + _dittoManager.Ditto.Sync.RegisterSubscription(SubscriptionQuery); + } + + /// + /// Adds a new task to the 'tasks' collection. + /// + public async Task AddTask(string title) + { + if (string.IsNullOrWhiteSpace(title)) + { + throw new ArgumentException("title cannot be empty"); + } + + var doc = new Dictionary + { + {"title", title}, + {"done", false}, + {"deleted", false } + }; + const string insertCommand = "INSERT INTO tasks DOCUMENTS (:task)"; + await _dittoManager.Ditto.Store.ExecuteAsync(insertCommand, new Dictionary() + { + { "task", doc } + }); + } + + /// + /// Update the title of an existing task. + /// + public async Task UpdateTaskTitle(string taskId, string newTitle) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + if (string.IsNullOrWhiteSpace(newTitle)) + { + throw new ArgumentException("title cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET title = :title WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + {"title", newTitle}, + {"id", taskId} + }); + } + + /// + /// Mark a task as deleted. + /// + public async Task DeleteTask(string taskId) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET deleted = true WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + { "id", taskId } + }); + } + + /// + /// Mark a task as complete or not complete. + /// + public async Task UpdateTaskDone(string taskId, bool newDoneState) + { + const string updateQuery = "UPDATE tasks SET done = :done WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary + { + { "done", newDoneState }, + { "id", taskId } + }); + } + + /// + /// Specify a handler to be called asynchronously when the task collection changes. + /// + public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) + { + // Register an observer, which runs against the local database on this device + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + return _dittoManager.Ditto.Store.RegisterObserver(ObserverQuery, async (queryResult) => + { + try + { + // Deserialize the JSON documents into TaskModel objects + var tasks = queryResult.Items.Select(d => + { + using (var reader = new StringReader(d.JsonString())) + using (var jsonReader = new JsonTextReader(reader)) + { + return JsonSerializer.Create().Deserialize(jsonReader); + } + }).ToList(); + + await handler(tasks); + } + catch (Exception e) + { + Console.Error.WriteLine($"ERROR tasks observation handler failed: {e.Message}"); + } + }); + } + } +} diff --git a/dotnet-winforms-net48/packages.config b/dotnet-winforms-net48/packages.config index df16735fe..098670775 100644 --- a/dotnet-winforms-net48/packages.config +++ b/dotnet-winforms-net48/packages.config @@ -1,21 +1,21 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet-winforms/IntegrationTest/IntegrationTest.csproj b/dotnet-winforms/IntegrationTest/IntegrationTest.csproj index 3736034f9..b660f2590 100644 --- a/dotnet-winforms/IntegrationTest/IntegrationTest.csproj +++ b/dotnet-winforms/IntegrationTest/IntegrationTest.csproj @@ -14,8 +14,9 @@ - - + + + diff --git a/dotnet-winforms/IntegrationTest/Program.cs b/dotnet-winforms/IntegrationTest/Program.cs index 5d7bfded4..59fcc50f5 100644 --- a/dotnet-winforms/IntegrationTest/Program.cs +++ b/dotnet-winforms/IntegrationTest/Program.cs @@ -48,24 +48,24 @@ static async Task Main(string[] args) } // Get required environment variables - if (!envVars.TryGetValue("DITTO_APP_ID", out var appId) || string.IsNullOrEmpty(appId)) + if (!envVars.TryGetValue("DITTO_DATABASE_ID", out var databaseId) || string.IsNullOrEmpty(databaseId)) { - Console.WriteLine("āŒ Missing DITTO_APP_ID in .env file"); + Console.WriteLine("āŒ Missing DITTO_DATABASE_ID in .env file"); return 1; } - if (!envVars.TryGetValue("DITTO_PLAYGROUND_TOKEN", out var playgroundToken) || string.IsNullOrEmpty(playgroundToken)) + if (!envVars.TryGetValue("DITTO_DEVELOPMENT_TOKEN", out var developmentToken) || string.IsNullOrEmpty(developmentToken)) { - Console.WriteLine("āŒ Missing DITTO_PLAYGROUND_TOKEN in .env file"); + Console.WriteLine("āŒ Missing DITTO_DEVELOPMENT_TOKEN in .env file"); return 1; } - var authUrl = envVars.GetValueOrDefault("DITTO_AUTH_URL", "https://auth.cloud.ditto.live"); - var websocketUrl = envVars.GetValueOrDefault("DITTO_WEBSOCKET_URL", "wss://cloud.ditto.live"); + var serverUrl = envVars.GetValueOrDefault("DITTO_SERVER_URL", "https://auth.cloud.ditto.live"); - Console.WriteLine($"šŸ“” Connecting to Ditto (App ID: {appId})"); + Console.WriteLine($"šŸ“” Connecting to Ditto (Database ID: {databaseId})"); - // Initialize TasksPeer and start sync - var tasksPeer = await TasksPeer.Create(appId, playgroundToken, authUrl, websocketUrl); + // Initialize Ditto and start sync + var dittoManager = await DittoManager.Create(databaseId, developmentToken, serverUrl); + var tasksRepository = new TasksRepository(dittoManager); Console.WriteLine("āœ… Ditto initialized and sync started"); Console.WriteLine("ā³ Waiting for document to sync..."); @@ -74,7 +74,7 @@ static async Task Main(string[] args) var maxWaitTime = TimeSpan.FromSeconds(30); var startTime = DateTime.Now; var found = false; - var foundTask = null as ToDoTask; + var foundTask = null as TaskModel; var taskCount = 0; var lastCheckTime = DateTime.Now; @@ -83,7 +83,7 @@ static async Task Main(string[] args) var cts = new CancellationTokenSource(maxWaitTime); // Register observer to watch for changes - var observer = tasksPeer.ObserveTasksCollection((tasks) => Task.Run(() => + var observer = tasksRepository.ObserveTasksCollection((tasks) => Task.Run(() => { taskCount = tasks.Count; var elapsed = (int)(DateTime.Now - startTime).TotalSeconds; @@ -123,7 +123,7 @@ static async Task Main(string[] args) // Cleanup // Note: DittoStoreObserver doesn't have a Stop/Dispose method - tasksPeer.Dispose(); + dittoManager.Dispose(); if (!found) { @@ -147,4 +147,4 @@ static async Task Main(string[] args) return 1; } } -} \ No newline at end of file +} diff --git a/dotnet-winforms/README.md b/dotnet-winforms/README.md index 3c9ea476f..eb798d792 100644 --- a/dotnet-winforms/README.md +++ b/dotnet-winforms/README.md @@ -3,14 +3,14 @@ ## Prerequisites 1. Install the .NET 10 SDK from -2. Create an application at . Make note of the app ID and online playground token -3. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your app ID and online playground token. +2. Create an application at . Make note of the Database ID and development token +3. Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and add your Database ID and development token. ## Documentation - [Ditto C# .NET SDK Install Guide](https://docs.ditto.live/install-guides/c-sharp) -- [Ditto C# .NET SDK API Reference](https://software.ditto.live/dotnet/Ditto/5.0.0/api-reference/) +- [Ditto C# .NET SDK API Reference](https://docs.ditto.live/sdk/latest/api-reference/c-sharp) ## .NET Windows Forms Application diff --git a/dotnet-winforms/TasksApp/AboutForm.Designer.cs b/dotnet-winforms/TasksApp/AboutForm.Designer.cs index 3fb677aa7..cf66bfefd 100644 --- a/dotnet-winforms/TasksApp/AboutForm.Designer.cs +++ b/dotnet-winforms/TasksApp/AboutForm.Designer.cs @@ -40,14 +40,12 @@ private void InitializeComponent() llDittoDocs = new LinkLabel(); lblSDKVersionText = new Label(); lblSDKVersion = new Label(); - lblAppIDText = new Label(); - lblAppIdValue = new Label(); - lblOnlinePlaygroundTokenTxt = new Label(); - lblOnlinePlaygroundTokenValue = new Label(); - lblAuthUrlTxt = new Label(); - lblAuthURLValue = new Label(); - lblWebsocketUrlTxt = new Label(); - lblWebsocketURLValue = new Label(); + lblDatabaseIdText = new Label(); + lblDatabaseIdValue = new Label(); + lblDevelopmentTokenTxt = new Label(); + lblDevelopmentTokenValue = new Label(); + lblServerUrlTxt = new Label(); + lblServerURLValue = new Label(); btnClose = new Button(); ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); SuspendLayout(); @@ -94,86 +92,66 @@ private void InitializeComponent() lblSDKVersion.TabIndex = 3; lblSDKVersion.Text = "0.0.0.0"; // - // lblAppIDText - // - lblAppIDText.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblAppIDText.AutoSize = true; - lblAppIDText.Location = new Point(12, 196); - lblAppIDText.Name = "lblAppIDText"; - lblAppIDText.Size = new Size(42, 15); - lblAppIDText.TabIndex = 4; - lblAppIDText.Text = "AppId:"; - // - // lblAppIdValue - // - lblAppIdValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblAppIdValue.AutoSize = true; - lblAppIdValue.Location = new Point(189, 196); - lblAppIdValue.Name = "lblAppIdValue"; - lblAppIdValue.Size = new Size(46, 15); - lblAppIdValue.TabIndex = 5; - lblAppIdValue.Text = "Not Set"; - // - // lblOnlinePlaygroundTokenTxt - // - lblOnlinePlaygroundTokenTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblOnlinePlaygroundTokenTxt.AutoSize = true; - lblOnlinePlaygroundTokenTxt.Location = new Point(12, 225); - lblOnlinePlaygroundTokenTxt.Name = "lblOnlinePlaygroundTokenTxt"; - lblOnlinePlaygroundTokenTxt.Size = new Size(144, 15); - lblOnlinePlaygroundTokenTxt.TabIndex = 6; - lblOnlinePlaygroundTokenTxt.Text = "Online Playground Token:"; - // - // lblOnlinePlaygroundTokenValue - // - lblOnlinePlaygroundTokenValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblOnlinePlaygroundTokenValue.AutoSize = true; - lblOnlinePlaygroundTokenValue.Location = new Point(189, 225); - lblOnlinePlaygroundTokenValue.Name = "lblOnlinePlaygroundTokenValue"; - lblOnlinePlaygroundTokenValue.Size = new Size(46, 15); - lblOnlinePlaygroundTokenValue.TabIndex = 7; - lblOnlinePlaygroundTokenValue.Text = "Not Set"; - // - // lblAuthUrlTxt - // - lblAuthUrlTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblAuthUrlTxt.AutoSize = true; - lblAuthUrlTxt.Location = new Point(12, 254); - lblAuthUrlTxt.Name = "lblAuthUrlTxt"; - lblAuthUrlTxt.Size = new Size(60, 15); - lblAuthUrlTxt.TabIndex = 8; - lblAuthUrlTxt.Text = "Auth URL:"; - // - // lblAuthURLValue - // - lblAuthURLValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblAuthURLValue.AutoSize = true; - lblAuthURLValue.Location = new Point(189, 254); - lblAuthURLValue.Name = "lblAuthURLValue"; - lblAuthURLValue.Size = new Size(46, 15); - lblAuthURLValue.TabIndex = 9; - lblAuthURLValue.Text = "Not Set"; - // - // lblWebsocketUrlTxt - // - lblWebsocketUrlTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblWebsocketUrlTxt.AutoSize = true; - lblWebsocketUrlTxt.Location = new Point(12, 283); - lblWebsocketUrlTxt.Name = "lblWebsocketUrlTxt"; - lblWebsocketUrlTxt.Size = new Size(92, 15); - lblWebsocketUrlTxt.TabIndex = 10; - lblWebsocketUrlTxt.Text = "Websocket URL:"; - // - // lblWebsocketURLValue - // - lblWebsocketURLValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - lblWebsocketURLValue.AutoSize = true; - lblWebsocketURLValue.Location = new Point(189, 283); - lblWebsocketURLValue.Name = "lblWebsocketURLValue"; - lblWebsocketURLValue.Size = new Size(46, 15); - lblWebsocketURLValue.TabIndex = 11; - lblWebsocketURLValue.Text = "Not Set"; - // + // lblDatabaseIdText + // + lblDatabaseIdText.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblDatabaseIdText.AutoSize = true; + lblDatabaseIdText.Location = new Point(12, 196); + lblDatabaseIdText.Name = "lblDatabaseIdText"; + lblDatabaseIdText.Size = new Size(42, 15); + lblDatabaseIdText.TabIndex = 4; + lblDatabaseIdText.Text = "Database ID:"; + // + // lblDatabaseIdValue + // + lblDatabaseIdValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblDatabaseIdValue.AutoSize = true; + lblDatabaseIdValue.Location = new Point(189, 196); + lblDatabaseIdValue.Name = "lblDatabaseIdValue"; + lblDatabaseIdValue.Size = new Size(46, 15); + lblDatabaseIdValue.TabIndex = 5; + lblDatabaseIdValue.Text = "Not Set"; + // + // lblDevelopmentTokenTxt + // + lblDevelopmentTokenTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblDevelopmentTokenTxt.AutoSize = true; + lblDevelopmentTokenTxt.Location = new Point(12, 225); + lblDevelopmentTokenTxt.Name = "lblDevelopmentTokenTxt"; + lblDevelopmentTokenTxt.Size = new Size(144, 15); + lblDevelopmentTokenTxt.TabIndex = 6; + lblDevelopmentTokenTxt.Text = "Development Token:"; + // + // lblDevelopmentTokenValue + // + lblDevelopmentTokenValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblDevelopmentTokenValue.AutoSize = true; + lblDevelopmentTokenValue.Location = new Point(189, 225); + lblDevelopmentTokenValue.Name = "lblDevelopmentTokenValue"; + lblDevelopmentTokenValue.Size = new Size(46, 15); + lblDevelopmentTokenValue.TabIndex = 7; + lblDevelopmentTokenValue.Text = "Not Set"; + // + // lblServerUrlTxt + // + lblServerUrlTxt.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblServerUrlTxt.AutoSize = true; + lblServerUrlTxt.Location = new Point(12, 254); + lblServerUrlTxt.Name = "lblServerUrlTxt"; + lblServerUrlTxt.Size = new Size(60, 15); + lblServerUrlTxt.TabIndex = 8; + lblServerUrlTxt.Text = "Server URL:"; + // + // lblServerURLValue + // + lblServerURLValue.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lblServerURLValue.AutoSize = true; + lblServerURLValue.Location = new Point(189, 254); + lblServerURLValue.Name = "lblServerURLValue"; + lblServerURLValue.Size = new Size(46, 15); + lblServerURLValue.TabIndex = 9; + lblServerURLValue.Text = "Not Set"; + // // btnClose // btnClose.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; @@ -192,14 +170,12 @@ private void InitializeComponent() AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(584, 361); Controls.Add(btnClose); - Controls.Add(lblWebsocketURLValue); - Controls.Add(lblWebsocketUrlTxt); - Controls.Add(lblAuthURLValue); - Controls.Add(lblAuthUrlTxt); - Controls.Add(lblOnlinePlaygroundTokenValue); - Controls.Add(lblOnlinePlaygroundTokenTxt); - Controls.Add(lblAppIdValue); - Controls.Add(lblAppIDText); + Controls.Add(lblServerURLValue); + Controls.Add(lblServerUrlTxt); + Controls.Add(lblDevelopmentTokenValue); + Controls.Add(lblDevelopmentTokenTxt); + Controls.Add(lblDatabaseIdValue); + Controls.Add(lblDatabaseIdText); Controls.Add(lblSDKVersion); Controls.Add(lblSDKVersionText); Controls.Add(llDittoDocs); @@ -223,14 +199,12 @@ private void InitializeComponent() private LinkLabel llDittoDocs; private Label lblSDKVersionText; private Label lblSDKVersion; - private Label lblAppIDText; - private Label lblAppIdValue; - private Label lblOnlinePlaygroundTokenTxt; - private Label lblOnlinePlaygroundTokenValue; - private Label lblAuthUrlTxt; - private Label lblAuthURLValue; - private Label lblWebsocketUrlTxt; - private Label lblWebsocketURLValue; + private Label lblDatabaseIdText; + private Label lblDatabaseIdValue; + private Label lblDevelopmentTokenTxt; + private Label lblDevelopmentTokenValue; + private Label lblServerUrlTxt; + private Label lblServerURLValue; private Button btnClose; } } \ No newline at end of file diff --git a/dotnet-winforms/TasksApp/AboutForm.cs b/dotnet-winforms/TasksApp/AboutForm.cs index 8d2f72fa0..3f9094ee1 100644 --- a/dotnet-winforms/TasksApp/AboutForm.cs +++ b/dotnet-winforms/TasksApp/AboutForm.cs @@ -13,24 +13,23 @@ namespace DittoTasksApp public partial class AboutForm : Form { - private readonly TasksPeer _tasksPeer; + private readonly DittoManager _dittoManager; - public AboutForm(TasksPeer tasksPeer) + public AboutForm(DittoManager dittoManager) { - _tasksPeer = tasksPeer; + _dittoManager = dittoManager; InitializeComponent(); SetFormValues(); } /// - /// Sets the values of the form labels based on the TasksPeer instance. + /// Sets the values of the form labels based on the DittoManager instance. /// private void SetFormValues() { - lblAppIdValue.Text = _tasksPeer.AppId; - lblAuthURLValue.Text = _tasksPeer.AuthUrl; - lblOnlinePlaygroundTokenValue.Text = _tasksPeer.PlaygroundToken; - lblWebsocketURLValue.Text = _tasksPeer.WebsocketUrl; + lblDatabaseIdValue.Text = _dittoManager.DatabaseId; + lblServerURLValue.Text = _dittoManager.ServerUrl; + lblDevelopmentTokenValue.Text = _dittoManager.DevelopmentToken; // Get the version of the DittoSDK assembly var dittoAssembly = typeof(DittoSDK.Ditto).Assembly; diff --git a/dotnet-winforms/TasksApp/DittoManager.cs b/dotnet-winforms/TasksApp/DittoManager.cs new file mode 100644 index 000000000..8897500cb --- /dev/null +++ b/dotnet-winforms/TasksApp/DittoManager.cs @@ -0,0 +1,118 @@ +using System; +using System.Threading.Tasks; + +using DittoSDK; +using DittoSDK.Auth; + +namespace DittoTasksApp +{ + /// + /// Manages the lifecycle and configuration of a Ditto instance: config, open, + /// auth, and starting/stopping sync. It vends a configured, already-running + /// instance (via the property) for the + /// tasks repository to use directly. + /// + /// + /// This class deliberately does NOT wrap Ditto's APIs (store/sync/etc.). The + /// repository calls the real Ditto API — e.g. dittoManager.Ditto.Store.ExecuteAsync(...) + /// — so the quickstart shows how to use Ditto directly rather than modeling an + /// abstraction layer over it. + /// + public class DittoManager : IDisposable + { + public string DatabaseId { get; private set; } + public string DevelopmentToken { get; private set; } + public string ServerUrl { get; private set; } + + public bool IsSyncActive => _ditto.Sync.IsActive; + + private readonly Ditto _ditto; + + /// + /// The configured, already-running Ditto instance. The tasks repository uses + /// this to call the real Ditto API directly. + /// + public Ditto Ditto => _ditto; + + /// + /// Creates a new synchronizing DittoManager instance. + /// + public static Task Create( + string databaseId, + string developmentToken, + string serverUrl) + { + var dittoManager = new DittoManager(databaseId, developmentToken, serverUrl); + dittoManager.Authenticate(); + dittoManager.StartSync(); + + return Task.FromResult(dittoManager); + } + + private void Authenticate() + { + _ditto.Auth.ExpirationHandler = async (ditto, secondsRemaining) => + { + // Authenticate when token is expiring + try + { + await ditto.Auth.LoginAsync( + // Your development token, replace with your actual token + DevelopmentToken, + // Use DittoAuthenticationProvider.Development, or your actual provider + DittoAuthenticationProvider.Development + ); + Console.WriteLine("Authentication successful"); + } + catch (Exception error) + { + Console.WriteLine($"Authentication failed: {error}"); + } + }; + } + + /// + /// Constructor + /// + /// Ditto database ID + /// Ditto development token + /// Ditto Server URL + private DittoManager(string databaseId, string developmentToken, string serverUrl) + { + DatabaseId = databaseId; + DevelopmentToken = developmentToken; + ServerUrl = serverUrl; + + var config = new DittoConfig( + DatabaseId, + new DittoConfigConnect.Server(new Uri(serverUrl)) + ); + + _ditto = Ditto.Open(config); + } + + public void Dispose() + { + _ditto.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// Start synchronizing with other peers and the Ditto server. + /// + public void StartSync() + { + _ditto.Sync.Start(); + } + + /// + /// Stop synchronizing. + /// + public void StopSync() + { + // Stop the sync engine only. The subscription is owned by TasksRepository + // and stays registered across a sync on/off toggle. + _ditto.Sync.Stop(); + } + } +} diff --git a/dotnet-winforms/TasksApp/DittoTasksApp.csproj b/dotnet-winforms/TasksApp/DittoTasksApp.csproj index cc3a97312..8791234a7 100644 --- a/dotnet-winforms/TasksApp/DittoTasksApp.csproj +++ b/dotnet-winforms/TasksApp/DittoTasksApp.csproj @@ -11,7 +11,7 @@ - + diff --git a/dotnet-winforms/TasksApp/MainForm.Designer.cs b/dotnet-winforms/TasksApp/MainForm.Designer.cs index bec6f5f0f..4f480ad54 100644 --- a/dotnet-winforms/TasksApp/MainForm.Designer.cs +++ b/dotnet-winforms/TasksApp/MainForm.Designer.cs @@ -33,7 +33,7 @@ private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); statusStrip1 = new StatusStrip(); - tsslAppId = new ToolStripStatusLabel(); + tsslDatabaseId = new ToolStripStatusLabel(); tsslAuthToken = new ToolStripStatusLabel(); menuStrip1 = new MenuStrip(); fileToolStripMenuItem = new ToolStripMenuItem(); @@ -68,19 +68,19 @@ private void InitializeComponent() // // statusStrip1 // - statusStrip1.Items.AddRange(new ToolStripItem[] { tsslAppId, tsslAuthToken }); + statusStrip1.Items.AddRange(new ToolStripItem[] { tsslDatabaseId, tsslAuthToken }); statusStrip1.Location = new Point(0, 707); statusStrip1.Name = "statusStrip1"; statusStrip1.Size = new Size(1008, 22); statusStrip1.TabIndex = 0; statusStrip1.Text = "statusStrip1"; // - // tsslAppId + // tsslDatabaseId // - tsslAppId.Name = "tsslAppId"; - tsslAppId.Padding = new Padding(0, 0, 40, 0); - tsslAppId.Size = new Size(82, 17); - tsslAppId.Text = "AppId:"; + tsslDatabaseId.Name = "tsslDatabaseId"; + tsslDatabaseId.Padding = new Padding(0, 0, 40, 0); + tsslDatabaseId.Size = new Size(82, 17); + tsslDatabaseId.Text = "Database ID:"; // // tsslAuthToken // @@ -334,7 +334,7 @@ private void InitializeComponent() #endregion private StatusStrip statusStrip1; - private ToolStripStatusLabel tsslAppId; + private ToolStripStatusLabel tsslDatabaseId; private ToolStripStatusLabel tsslAuthToken; private MenuStrip menuStrip1; private ToolStripMenuItem fileToolStripMenuItem; diff --git a/dotnet-winforms/TasksApp/MainForm.cs b/dotnet-winforms/TasksApp/MainForm.cs index b8e86546d..0a1073914 100644 --- a/dotnet-winforms/TasksApp/MainForm.cs +++ b/dotnet-winforms/TasksApp/MainForm.cs @@ -7,23 +7,25 @@ namespace DittoTasksApp public partial class MainForm : Form { - private readonly TasksPeer _tasksPeer; + private readonly DittoManager _dittoManager; + private readonly TasksRepository _tasksRepository; private DittoStoreObserver _tasksObserver; private bool _isSyncEnabled = true; private readonly string _imageSyncOn = "assets/sync-on.bmp"; private readonly string _imageSyncOff = "assets/sync-off.bmp"; - public MainForm(TasksPeer tasksPeer) + public MainForm(DittoManager dittoManager, TasksRepository tasksRepository) { - _tasksPeer = tasksPeer; - _tasksPeer.StartSync(); + _dittoManager = dittoManager; + _tasksRepository = tasksRepository; + _dittoManager.StartSync(); InitializeComponent(); SetFormValues(); // Observe the tasks collection - _tasksObserver = _tasksPeer.ObserveTasksCollection(async (tasks) => + _tasksObserver = _tasksRepository.ObserveTasksCollection(async (tasks) => { // Update the ListView on the UI thread Invoke(() => UpdateTaskListView(tasks)); @@ -32,8 +34,8 @@ public MainForm(TasksPeer tasksPeer) private void SetFormValues() { - tsslAppId.Text = $"AppId: {_tasksPeer.AppId}"; - tsslAuthToken.Text = $"Online Playground Token: {_tasksPeer.PlaygroundToken}"; + tsslDatabaseId.Text = $"Database ID: {_dittoManager.DatabaseId}"; + tsslAuthToken.Text = $"Development Token: {_dittoManager.DevelopmentToken}"; tsbSyncStatus.Image = Image.FromFile(_imageSyncOn); @@ -41,13 +43,13 @@ private void SetFormValues() private void LoadAddForm() { - var editorForm = new ToDoTaskEditorForm(_tasksPeer, null); + var editorForm = new TaskEditorForm(_tasksRepository, null); editorForm.Owner = this; editorForm.ShowDialog(); } - private void UpdateTaskListView(IList tasks) + private void UpdateTaskListView(IList tasks) { lvTasks.BeginUpdate(); lvTasks.Items.Clear(); @@ -74,8 +76,8 @@ private void UpdateTaskListView(IList tasks) private void exitToolStripMenuItem_Click(object sender, EventArgs e) { _tasksObserver.Cancel(); - _tasksPeer.StopSync(); - _tasksPeer.Dispose(); + _dittoManager.StopSync(); + _dittoManager.Dispose(); Application.Exit(); } @@ -83,7 +85,7 @@ private void exitToolStripMenuItem_Click(object sender, EventArgs e) private void aboutToolStripMenuItem_Click(object sender, EventArgs e) { - var aboutForm = new AboutForm(_tasksPeer); + var aboutForm = new AboutForm(_dittoManager); aboutForm.Owner = this; aboutForm.ShowDialog(); } @@ -100,10 +102,10 @@ private void tsbAdd_Click(object sender, EventArgs e) private void tsbEdit_Click(object sender, EventArgs e) { - var todoItem = (ToDoTask?)lvTasks.CheckedItems[0].Tag; + var todoItem = (TaskModel?)lvTasks.CheckedItems[0].Tag; if (todoItem != null) { - var editForm = new ToDoTaskEditorForm(_tasksPeer, todoItem); + var editForm = new TaskEditorForm(_tasksRepository, todoItem); editForm.Owner = this; editForm.ShowDialog(); } @@ -113,10 +115,10 @@ private async void tsbComplete_Click(object sender, EventArgs e) { foreach (ListViewItem item in lvTasks.CheckedItems) { - var task = (ToDoTask?)item.Tag; + var task = (TaskModel?)item.Tag; if (task != null) { - await _tasksPeer.UpdateTaskDone(task.Id, !task.Done); + await _tasksRepository.UpdateTaskDone(task.Id, !task.Done); } } } @@ -125,10 +127,10 @@ private async void tsbDelete_Click(object sender, EventArgs e) { foreach (ListViewItem item in lvTasks.CheckedItems) { - var task = (ToDoTask?)item.Tag; + var task = (TaskModel?)item.Tag; if (task != null) { - await _tasksPeer.DeleteTask(task.Id); + await _tasksRepository.DeleteTask(task.Id); } } @@ -140,12 +142,12 @@ private void tsbSyncStatus_Click(object sender, EventArgs e) if (_isSyncEnabled) { tsbSyncStatus.Image = Image.FromFile(_imageSyncOn); - _tasksPeer.StartSync(); + _dittoManager.StartSync(); } else { tsbSyncStatus.Image = Image.FromFile(_imageSyncOff); - _tasksPeer.StopSync(); + _dittoManager.StopSync(); } } diff --git a/dotnet-winforms/TasksApp/Program.cs b/dotnet-winforms/TasksApp/Program.cs index 6394c2e2e..931de8911 100644 --- a/dotnet-winforms/TasksApp/Program.cs +++ b/dotnet-winforms/TasksApp/Program.cs @@ -13,19 +13,19 @@ internal static class Program static async Task Main() { var env = LoadEnvVariables(); - var appId = env["DITTO_APP_ID"]; - var playgroundToken = env["DITTO_PLAYGROUND_TOKEN"]; - var websocketUrl = env["DITTO_WEBSOCKET_URL"]; - var authUrl = env["DITTO_AUTH_URL"]; + var databaseId = env["DITTO_DATABASE_ID"]; + var developmentToken = env["DITTO_DEVELOPMENT_TOKEN"]; + var serverUrl = env["DITTO_SERVER_URL"]; - using var peer = await TasksPeer.Create(appId, playgroundToken, authUrl, websocketUrl); + using var dittoManager = await DittoManager.Create(databaseId, developmentToken, serverUrl); + var tasksRepository = new TasksRepository(dittoManager); // Disable Ditto's standard-error logging DittoLogger.IsEnabled = true; // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); - Application.Run(new MainForm(peer)); + Application.Run(new MainForm(dittoManager, tasksRepository)); } /// @@ -81,4 +81,4 @@ private static Dictionary LoadEnvVariables() return envVars; } } -} \ No newline at end of file +} diff --git a/dotnet-winforms/TasksApp/ToDoTaskEditorForm.Designer.cs b/dotnet-winforms/TasksApp/TaskEditorForm.Designer.cs similarity index 96% rename from dotnet-winforms/TasksApp/ToDoTaskEditorForm.Designer.cs rename to dotnet-winforms/TasksApp/TaskEditorForm.Designer.cs index 5d5cd9e60..c12c837be 100644 --- a/dotnet-winforms/TasksApp/ToDoTaskEditorForm.Designer.cs +++ b/dotnet-winforms/TasksApp/TaskEditorForm.Designer.cs @@ -1,6 +1,6 @@ namespace DittoTasksApp { - partial class ToDoTaskEditorForm + partial class TaskEditorForm { /// /// Required designer variable. @@ -96,9 +96,9 @@ private void InitializeComponent() btnCancel2.Text = "Cancel"; btnCancel2.UseVisualStyleBackColor = true; btnCancel2.Click += btnCancel_Click; - // - // ToDoTaskEditorForm - // + // + // TaskEditorForm + // AutoScaleDimensions = new SizeF(17F, 41F); AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(1137, 443); @@ -111,7 +111,7 @@ private void InitializeComponent() Margin = new Padding(7, 8, 7, 8); MaximumSize = new Size(1169, 531); MinimumSize = new Size(1169, 531); - Name = "ToDoTaskEditorForm"; + Name = "TaskEditorForm"; StartPosition = FormStartPosition.CenterParent; Text = "ToDo Task Editor"; ResumeLayout(false); diff --git a/dotnet-winforms/TasksApp/ToDoTaskEditorForm.cs b/dotnet-winforms/TasksApp/TaskEditorForm.cs similarity index 69% rename from dotnet-winforms/TasksApp/ToDoTaskEditorForm.cs rename to dotnet-winforms/TasksApp/TaskEditorForm.cs index 75e5d178f..fcfe7d0aa 100644 --- a/dotnet-winforms/TasksApp/ToDoTaskEditorForm.cs +++ b/dotnet-winforms/TasksApp/TaskEditorForm.cs @@ -10,14 +10,14 @@ namespace DittoTasksApp { - public partial class ToDoTaskEditorForm : Form + public partial class TaskEditorForm : Form { - private readonly TasksPeer _tasksPeer; - private readonly ToDoTask? _task; - public ToDoTaskEditorForm(TasksPeer tasksPeer, ToDoTask? task) + private readonly TasksRepository _tasksRepository; + private readonly TaskModel? _task; + public TaskEditorForm(TasksRepository tasksRepository, TaskModel? task) { _task = task; - _tasksPeer = tasksPeer; + _tasksRepository = tasksRepository; InitializeComponent(); SetFormValues(); @@ -41,13 +41,13 @@ private async void btnSave_Click(object sender, EventArgs e) { if (_task == null) { - await _tasksPeer.AddTask(tbName.Text); + await _tasksRepository.AddTask(tbName.Text); CloseForm(); } else { - await _tasksPeer.UpdateTaskTitle(_task.Id, tbName.Text); - await _tasksPeer.UpdateTaskDone(_task.Id, cbIsCompleted.Checked); + await _tasksRepository.UpdateTaskTitle(_task.Id, tbName.Text); + await _tasksRepository.UpdateTaskDone(_task.Id, cbIsCompleted.Checked); CloseForm(); } diff --git a/dotnet-winforms/TasksApp/ToDoTaskEditorForm.resx b/dotnet-winforms/TasksApp/TaskEditorForm.resx similarity index 100% rename from dotnet-winforms/TasksApp/ToDoTaskEditorForm.resx rename to dotnet-winforms/TasksApp/TaskEditorForm.resx diff --git a/dotnet-winforms/TasksApp/ToDoTask.cs b/dotnet-winforms/TasksApp/TaskModel.cs similarity index 86% rename from dotnet-winforms/TasksApp/ToDoTask.cs rename to dotnet-winforms/TasksApp/TaskModel.cs index 4b2af6f49..b150f8cf7 100644 --- a/dotnet-winforms/TasksApp/ToDoTask.cs +++ b/dotnet-winforms/TasksApp/TaskModel.cs @@ -6,10 +6,10 @@ namespace DittoTasksApp; /// Representation of a document in the Ditto 'tasks' collection. /// /// -/// This class is named ToDoTask rather than Task to avoid +/// This class is named TaskModel rather than Task to avoid /// conflicts with the System.Threading.Tasks.Task class. /// -public class ToDoTask +public class TaskModel { [JsonPropertyName("_id")] public string Id { get; set; } diff --git a/dotnet-winforms/TasksApp/TasksPeer.cs b/dotnet-winforms/TasksApp/TasksPeer.cs deleted file mode 100644 index 59dfe2935..000000000 --- a/dotnet-winforms/TasksApp/TasksPeer.cs +++ /dev/null @@ -1,290 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; - -using DittoSDK; -using DittoSDK.Auth; -using DittoSDK.Store; -using DittoTasksApp; - -/// -/// Encapsulates use of the Ditto SDK and the 'tasks' collection. -/// - -namespace DittoTasksApp -{ - public class TasksPeer : IDisposable - { - private const string Query = "SELECT * FROM tasks WHERE NOT deleted"; - - public string AppId { get; private set; } - public string PlaygroundToken { get; private set; } - public string AuthUrl { get; private set; } - public string WebsocketUrl { get; private set; } - - public bool IsSyncActive => _ditto.Sync.IsActive; - - private readonly Ditto _ditto; - - /// - /// Creates a new synchronizing TasksPeer instance. - /// - public static async Task Create( - string appId, - string playgroundToken, - string authUrl, - string websocketUrl) - { - var peer = new TasksPeer(appId, playgroundToken, authUrl, websocketUrl); - peer.Authenticate(); - peer.RegisterSubscription(); - await peer.InsertInitialTasks(); - peer.StartSync(); - - return peer; - } - - private void Authenticate() - { - _ditto.Auth.ExpirationHandler = async (ditto, secondsRemaining) => - { - // Authenticate when token is expiring - try - { - await ditto.Auth.LoginAsync( - // Your development token, replace with your actual token - PlaygroundToken, - // Use DittoAuthenticationProvider.Development for playground, or your actual provider - DittoAuthenticationProvider.Development - ); - Console.WriteLine("Authentication successful"); - } - catch (Exception error) - { - Console.WriteLine($"Authentication failed: {error}"); - } - }; - } - - /// - /// Registers a subscription for the tasks collection to enable data synchronization - /// with other peers and the Ditto cloud. This subscription determines what data - /// will be synced to this peer during the synchronization process. - /// - /// - /// The subscription is created using the same query that filters out deleted tasks - /// (SELECT * FROM tasks WHERE NOT deleted), ensuring that only active, - /// non-deleted tasks are synchronized across the network. - /// - /// This method should be called during peer initialization to establish the - /// subscription before starting the sync process. The subscription remains active - /// until explicitly cancelled or when the peer is disposed. - /// - /// - private void RegisterSubscription() - { - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - _ditto.Sync.RegisterSubscription(Query); - } - - /// - /// Constructor - /// - /// Ditto application ID - /// Ditto online playground token - /// Ditto Auth URL - /// Ditto Websocket URL - private TasksPeer(string appId, string playgroundToken, string authUrl, string websocketUrl) - { - AppId = appId; - PlaygroundToken = playgroundToken; - AuthUrl = authUrl; - WebsocketUrl = websocketUrl; - - var config = new DittoConfig( - AppId, - new DittoConfigConnect.Server(new Uri(authUrl)) - ); - - _ditto = Ditto.Open(config); - } - - public void Dispose() - { - _ditto.Dispose(); - GC.SuppressFinalize(this); - } - - /// - /// Inserts the initial tasks into the 'tasks' collection. - /// - private async Task InsertInitialTasks() - { - var initialTasks = new List> - { - new Dictionary - { - {"_id", "50191411-4C46-4940-8B72-5F8017A04FA7"}, - {"title", "Buy groceries"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "6DA283DA-8CFE-4526-A6FA-D385089364E5"}, - {"title", "Clean the kitchen"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "5303DDF8-0E72-4FEB-9E82-4B007E5797F0"}, - {"title", "Schedule dentist appointment"}, - {"done", false}, - {"deleted", false} - }, - new Dictionary - { - {"_id", "38411F1B-6B49-4346-90C3-0B16CE97E174"}, - {"title", "Pay bills"}, - {"done", false}, - {"deleted", false} - } - }; - - const string insertCommand = "INSERT INTO tasks INITIAL DOCUMENTS (:task)"; - foreach (var task in initialTasks) - { - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "task", task } - }); - } - } - - /// - /// Adds a new task to the 'tasks' collection. - /// - public async Task AddTask(string title) - { - if (string.IsNullOrWhiteSpace(title)) - { - throw new ArgumentException("title cannot be empty"); - } - - var doc = new Dictionary - { - {"title", title}, - {"done", false}, - {"deleted", false } - }; - const string insertCommand = "INSERT INTO tasks DOCUMENTS (:doc)"; - await _ditto.Store.ExecuteAsync(insertCommand, new Dictionary() - { - { "doc", doc } - }); - } - - /// - /// Update the title of an existing task. - /// - public async Task UpdateTaskTitle(string taskId, string newTitle) - { - if (string.IsNullOrWhiteSpace(taskId)) - { - throw new ArgumentException("taskId cannot be empty"); - } - - if (string.IsNullOrWhiteSpace(newTitle)) - { - throw new ArgumentException("title cannot be empty"); - } - - const string updateQuery = "UPDATE tasks SET title = :title WHERE _id = :id"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - {"title", newTitle}, - {"id", taskId} - }); - } - - /// - /// Mark a task as deleted. - /// - public async Task DeleteTask(string taskId) - { - if (string.IsNullOrWhiteSpace(taskId)) - { - throw new ArgumentException("taskId cannot be empty"); - } - - const string updateQuery = "UPDATE tasks SET deleted = true WHERE _id = :id"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary() - { - { "id", taskId } - }); - } - - /// - /// Mark a task as complete or not complete. - /// - public async Task UpdateTaskDone(string taskId, bool newDoneState) - { - const string updateQuery = "UPDATE tasks SET done = :newDoneState WHERE _id = :id AND done != :newDoneState"; - await _ditto.Store.ExecuteAsync(updateQuery, new Dictionary - { - { "newDoneState", newDoneState }, - { "id", taskId } - }); - } - - /// - /// Specify a handler to be called asynchronously when the task collection changes. - /// - public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) - { - // Register an observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - return _ditto.Store.RegisterObserver(Query, async (queryResult) => - { - try - { - // Deserialize the JSON documents into ToDoTask objects - var tasks = queryResult.Items.Select(d => - JsonSerializer.Deserialize(d.JsonString()) - ).OrderBy(t => t?.Id).ToList(); - - await handler(tasks); - } - catch (Exception e) - { - Console.Error.WriteLine($"ERROR tasks observation handler failed: {e.Message}"); - } - }); - } - - /// - /// Start synchronizing the 'tasks' collection. - /// - public void StartSync() - { - _ditto.Sync.Start(); - } - - /// - /// Stop synchronizing the 'tasks' collection. - /// - public void StopSync() - { - foreach (var subscription in _ditto.Sync.Subscriptions) - { - subscription.Cancel(); - } - _ditto.Sync.Stop(); - } - } -} diff --git a/dotnet-winforms/TasksApp/TasksRepository.cs b/dotnet-winforms/TasksApp/TasksRepository.cs new file mode 100644 index 000000000..435722157 --- /dev/null +++ b/dotnet-winforms/TasksApp/TasksRepository.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +using DittoSDK.Store; + +namespace DittoTasksApp +{ + /// + /// Owns all 'tasks' collection data operations: the DQL query strings, the sync + /// subscription, the store observer, and CRUD. It calls the real Ditto API + /// directly through the it is given. + /// + public class TasksRepository + { + // The subscription controls what syncs to this device and is written to + // the local database; the observer reacts to changes in that local + // database, hiding soft-deleted tasks from the displayed list. + private const string SubscriptionQuery = "SELECT * FROM tasks"; + private const string ObserverQuery = "SELECT * FROM tasks WHERE NOT deleted"; + + private readonly DittoManager _dittoManager; + + public TasksRepository(DittoManager dittoManager) + { + _dittoManager = dittoManager; + RegisterSubscription(); + } + + /// + /// Registers a subscription for the tasks collection to enable data synchronization + /// with other peers and the Ditto server. This subscription determines what data + /// will be synced to this device during the synchronization process. + /// + /// + /// The subscription syncs all tasks (SELECT * FROM tasks) and controls + /// what is written to the local database; the observer query hides + /// soft-deleted tasks from the displayed list. + /// + /// + private void RegisterSubscription() + { + // Register a subscription, which determines what data syncs to this device + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + _dittoManager.Ditto.Sync.RegisterSubscription(SubscriptionQuery); + } + + /// + /// Adds a new task to the 'tasks' collection. + /// + public async Task AddTask(string title) + { + if (string.IsNullOrWhiteSpace(title)) + { + throw new ArgumentException("title cannot be empty"); + } + + var doc = new Dictionary + { + {"title", title}, + {"done", false}, + {"deleted", false } + }; + const string insertCommand = "INSERT INTO tasks DOCUMENTS (:task)"; + await _dittoManager.Ditto.Store.ExecuteAsync(insertCommand, new Dictionary() + { + { "task", doc } + }); + } + + /// + /// Update the title of an existing task. + /// + public async Task UpdateTaskTitle(string taskId, string newTitle) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + if (string.IsNullOrWhiteSpace(newTitle)) + { + throw new ArgumentException("title cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET title = :title WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + {"title", newTitle}, + {"id", taskId} + }); + } + + /// + /// Mark a task as deleted. + /// + public async Task DeleteTask(string taskId) + { + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("taskId cannot be empty"); + } + + const string updateQuery = "UPDATE tasks SET deleted = true WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary() + { + { "id", taskId } + }); + } + + /// + /// Mark a task as complete or not complete. + /// + public async Task UpdateTaskDone(string taskId, bool newDoneState) + { + const string updateQuery = "UPDATE tasks SET done = :done WHERE _id = :id"; + await _dittoManager.Ditto.Store.ExecuteAsync(updateQuery, new Dictionary + { + { "done", newDoneState }, + { "id", taskId } + }); + } + + /// + /// Specify a handler to be called asynchronously when the task collection changes. + /// + public DittoStoreObserver ObserveTasksCollection(Func, Task> handler) + { + // Register an observer, which runs against the local database on this device + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + return _dittoManager.Ditto.Store.RegisterObserver(ObserverQuery, async (queryResult) => + { + try + { + // Deserialize the JSON documents into TaskModel objects + var tasks = queryResult.Items.Select(d => + JsonSerializer.Deserialize(d.JsonString()) + ).ToList(); + + await handler(tasks); + } + catch (Exception e) + { + Console.Error.WriteLine($"ERROR tasks observation handler failed: {e.Message}"); + } + }); + } + } +} diff --git a/electron/README.md b/electron/README.md index b9ec00a9a..f2085279f 100644 --- a/electron/README.md +++ b/electron/README.md @@ -4,9 +4,9 @@ This directory contains Ditto's quickstart app for [Electron](https://www.electr ## Documentation -- [Javascript Install Guide](https://docs.ditto.com/sdk/latest/install-guides/js) -- [Javascript API Reference](https://software.ditto.live/js/Ditto/5.0.0/api-reference/) -- [Javascript Release Notes](https://docs.ditto.com/sdk/latest/release-notes/js) +- [Javascript Install Guide](https://docs.ditto.live/sdk/latest/install-guides/js) +- [Javascript API Reference](https://docs.ditto.live/sdk/latest/api-reference/js) +- [Javascript Release Notes](https://docs.ditto.live/sdk/latest/release-notes/js) ## Prerequisites @@ -36,7 +36,7 @@ The renderer never imports `@dittolive/ditto`; it only calls `window.ditto.*` me ### Peer-to-peer transports -This quickstart enables LAN sync (TCP + mDNS + multicast) and disables Bluetooth LE and AWDL. BLE and AWDL on macOS require entitlements that only signed app bundles get; a plain `npm run dev` Electron process can't use them. LAN P2P provides peer-to-peer sync between devices on the same network, and the `DITTO_WEBSOCKET_URL` provides cloud sync to Ditto's Big Peer. +This quickstart enables LAN sync (TCP + mDNS + multicast) and disables Bluetooth LE and AWDL. BLE and AWDL on macOS require entitlements that only signed app bundles get; a plain `npm run dev` Electron process can't use them. LAN P2P provides peer-to-peer sync between devices on the same network, and the `DITTO_SERVER_URL` provides sync to Ditto's Big Peer. ### Local storage @@ -50,7 +50,7 @@ Tasks survive restarts because of this. To reset, quit the app and delete that d ## Getting Started -To get started, you'll first need to create an app in the [Ditto Portal][0] with the "Online Playground" authentication type. You'll need your AppID, Playground Token, Auth URL, and WebSocket URL. +To get started, you'll first need to create an app in the [Ditto Portal][0] with the "Development" authentication type. You'll need your Database ID, Development Token, and Server URL. [0]: https://portal.ditto.live @@ -63,10 +63,9 @@ cp .env.sample .env The `.env` file should look like this (with your fields filled in): ```bash -DITTO_APP_ID="" -DITTO_PLAYGROUND_TOKEN="" -DITTO_AUTH_URL="" -DITTO_WEBSOCKET_URL="" +DITTO_DATABASE_ID="" +DITTO_DEVELOPMENT_TOKEN="" +DITTO_SERVER_URL="" ``` Then run the app: diff --git a/electron/package-lock.json b/electron/package-lock.json index 9501f5a7b..d8870e9fa 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -8,7 +8,7 @@ "name": "ditto-quickstart-electron", "version": "0.0.0", "dependencies": { - "@dittolive/ditto": "5.0.0", + "@dittolive/ditto": "5.0.3", "dotenv": "^16.4.5" }, "devDependencies": { @@ -16,10 +16,10 @@ "@types/node": "^20.14.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react-swc": "^3.5.0", + "@vitejs/plugin-react-swc": "^4.3.2", "autoprefixer": "^10.4.20", "electron": "^42.0.0", - "electron-vite": "^5.0.0", + "electron-vite": "^6.0.0-beta.1", "eslint": "^9.15.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.4.0", @@ -33,7 +33,7 @@ "tailwindcss": "^3.4.16", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", - "vite": "^6.4.2" + "vite": "^8.0.10" }, "engines": { "node": ">=22.12.0" @@ -319,12 +319,24 @@ } }, "node_modules/@dittolive/ditto": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@dittolive/ditto/-/ditto-5.0.0.tgz", - "integrity": "sha512-Qv4bXhaXbCJlazFa2nLsx6LLeFohBuT0GupfXo7R+nalNGv5kzrJMuafbawydIuaP7ZgGn2e+39eiSny8iRSHg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@dittolive/ditto/-/ditto-5.0.3.tgz", + "integrity": "sha512-CDz3kRhSvBPsq95HUcIdA0CONFUXU5cEfZ0r60k++y6lmjgReJMLtimDfGoUp2YQufuYZpfmcXNp1x/OITBcMQ==", "license": "SEE LICENSE IN LICENSE.md", "engines": { "node": ">=14" + }, + "peerDependencies": { + "@expo/config-plugins": "*", + "expo-build-properties": "*" + }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + }, + "expo-build-properties": { + "optional": true + } } }, "node_modules/@electron/get": { @@ -361,10 +373,44 @@ "node": ">=10" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -379,9 +425,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -396,9 +442,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -413,9 +459,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -430,9 +476,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -447,9 +493,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -464,9 +510,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -481,9 +527,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -498,9 +544,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -515,9 +561,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -532,9 +578,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -549,9 +595,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -566,9 +612,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -583,9 +629,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -600,9 +646,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -617,9 +663,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -634,9 +680,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -651,9 +697,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -668,9 +714,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -685,9 +731,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -702,9 +748,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -719,9 +765,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -736,9 +782,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -753,9 +799,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -770,9 +816,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -787,9 +833,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1076,6 +1122,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1114,6 +1179,16 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -1127,31 +1202,10 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", - "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", - "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -1160,12 +1214,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", - "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -1174,12 +1231,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", - "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -1188,26 +1248,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", - "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", - "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -1216,26 +1265,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", - "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", - "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -1244,180 +1282,135 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", - "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", - "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", - "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", - "cpu": [ - "loong64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", - "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", - "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", - "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", - "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", - "cpu": [ - "riscv64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", - "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", - "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", - "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", - "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", - "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", - "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -1426,40 +1419,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", - "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", - "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", - "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -1468,32 +1472,28 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", - "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz", - "integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.46.tgz", + "integrity": "sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.26" + "@swc/types": "^0.1.27" }, "engines": { "node": ">=10" @@ -1503,18 +1503,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.33", - "@swc/core-darwin-x64": "1.15.33", - "@swc/core-linux-arm-gnueabihf": "1.15.33", - "@swc/core-linux-arm64-gnu": "1.15.33", - "@swc/core-linux-arm64-musl": "1.15.33", - "@swc/core-linux-ppc64-gnu": "1.15.33", - "@swc/core-linux-s390x-gnu": "1.15.33", - "@swc/core-linux-x64-gnu": "1.15.33", - "@swc/core-linux-x64-musl": "1.15.33", - "@swc/core-win32-arm64-msvc": "1.15.33", - "@swc/core-win32-ia32-msvc": "1.15.33", - "@swc/core-win32-x64-msvc": "1.15.33" + "@swc/core-darwin-arm64": "1.15.46", + "@swc/core-darwin-x64": "1.15.46", + "@swc/core-linux-arm-gnueabihf": "1.15.46", + "@swc/core-linux-arm64-gnu": "1.15.46", + "@swc/core-linux-arm64-musl": "1.15.46", + "@swc/core-linux-ppc64-gnu": "1.15.46", + "@swc/core-linux-s390x-gnu": "1.15.46", + "@swc/core-linux-x64-gnu": "1.15.46", + "@swc/core-linux-x64-musl": "1.15.46", + "@swc/core-win32-arm64-msvc": "1.15.46", + "@swc/core-win32-ia32-msvc": "1.15.46", + "@swc/core-win32-x64-msvc": "1.15.46" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1526,9 +1526,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz", - "integrity": "sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.46.tgz", + "integrity": "sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==", "cpu": [ "arm64" ], @@ -1543,9 +1543,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz", - "integrity": "sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.46.tgz", + "integrity": "sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==", "cpu": [ "x64" ], @@ -1560,9 +1560,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz", - "integrity": "sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.46.tgz", + "integrity": "sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==", "cpu": [ "arm" ], @@ -1577,13 +1577,16 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz", - "integrity": "sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.46.tgz", + "integrity": "sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1594,13 +1597,16 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz", - "integrity": "sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.46.tgz", + "integrity": "sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1611,13 +1617,16 @@ } }, "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz", - "integrity": "sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.46.tgz", + "integrity": "sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1628,13 +1637,16 @@ } }, "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz", - "integrity": "sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.46.tgz", + "integrity": "sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1645,13 +1657,16 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz", - "integrity": "sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.46.tgz", + "integrity": "sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1662,13 +1677,16 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz", - "integrity": "sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.46.tgz", + "integrity": "sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1679,9 +1697,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz", - "integrity": "sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.46.tgz", + "integrity": "sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==", "cpu": [ "arm64" ], @@ -1696,9 +1714,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz", - "integrity": "sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.46.tgz", + "integrity": "sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==", "cpu": [ "ia32" ], @@ -1713,9 +1731,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.33", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz", - "integrity": "sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.46.tgz", + "integrity": "sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==", "cpu": [ "x64" ], @@ -1737,15 +1755,26 @@ "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", - "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2105,17 +2134,20 @@ } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", - "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.2.tgz", + "integrity": "sha512-MlSlmSAYbYwPjGa+CjOAqwij09iPYAQDHwx8osVkwoDamCxXlg+avpkC65Ysv+L/uqUWvRTsnJCKPjVhZih/sA==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.27", - "@swc/core": "^1.12.11" + "@rolldown/pluginutils": "^1.0.1", + "@swc/core": "^1.15.46" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7" + "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "node_modules/acorn": { @@ -2348,13 +2380,13 @@ } }, "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=20.19.0" } }, "node_modules/callsites": { @@ -2557,6 +2589,16 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2610,17 +2652,17 @@ "license": "ISC" }, "node_modules/electron-vite": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-5.0.0.tgz", - "integrity": "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==", + "version": "6.0.0-beta.1", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-6.0.0-beta.1.tgz", + "integrity": "sha512-jltST77AwNxIeTTDtYhnIEA8ZM0RW9jmSJcqS8x/dQcgeeLlxlcJoYNNJ1tv7/Swor0AbXMYteBGdezoAOt+Nw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.4", + "@babel/core": "^7.29.0", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "cac": "^6.7.14", + "cac": "^7.0.0", "esbuild": "^0.25.11", - "magic-string": "^0.30.19", + "magic-string": "^0.30.21", "picocolors": "^1.1.1" }, "bin": { @@ -2631,7 +2673,7 @@ }, "peerDependencies": { "@swc/core": "^1.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@swc/core": { @@ -2690,9 +2732,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2703,32 +2745,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -3469,6 +3511,279 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3602,9 +3917,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -3812,9 +4127,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "dev": true, "funding": [ { @@ -3832,7 +4147,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4158,58 +4473,40 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", - "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.3", - "@rollup/rollup-android-arm64": "4.60.3", - "@rollup/rollup-darwin-arm64": "4.60.3", - "@rollup/rollup-darwin-x64": "4.60.3", - "@rollup/rollup-freebsd-arm64": "4.60.3", - "@rollup/rollup-freebsd-x64": "4.60.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", - "@rollup/rollup-linux-arm-musleabihf": "4.60.3", - "@rollup/rollup-linux-arm64-gnu": "4.60.3", - "@rollup/rollup-linux-arm64-musl": "4.60.3", - "@rollup/rollup-linux-loong64-gnu": "4.60.3", - "@rollup/rollup-linux-loong64-musl": "4.60.3", - "@rollup/rollup-linux-ppc64-gnu": "4.60.3", - "@rollup/rollup-linux-ppc64-musl": "4.60.3", - "@rollup/rollup-linux-riscv64-gnu": "4.60.3", - "@rollup/rollup-linux-riscv64-musl": "4.60.3", - "@rollup/rollup-linux-s390x-gnu": "4.60.3", - "@rollup/rollup-linux-x64-gnu": "4.60.3", - "@rollup/rollup-linux-x64-musl": "4.60.3", - "@rollup/rollup-openbsd-x64": "4.60.3", - "@rollup/rollup-openharmony-arm64": "4.60.3", - "@rollup/rollup-win32-arm64-msvc": "4.60.3", - "@rollup/rollup-win32-ia32-msvc": "4.60.3", - "@rollup/rollup-win32-x64-gnu": "4.60.3", - "@rollup/rollup-win32-x64-msvc": "4.60.3", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4440,9 +4737,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -4520,6 +4817,14 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4572,9 +4877,9 @@ } }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "optional": true, @@ -4638,24 +4943,23 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4664,14 +4968,15 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -4680,13 +4985,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -4712,28 +5020,10 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { diff --git a/electron/package.json b/electron/package.json index b7bdea6ea..ddad1baf7 100644 --- a/electron/package.json +++ b/electron/package.json @@ -20,7 +20,7 @@ "format": "prettier --write ." }, "dependencies": { - "@dittolive/ditto": "5.0.0", + "@dittolive/ditto": "5.0.3", "dotenv": "^16.4.5" }, "devDependencies": { @@ -28,10 +28,10 @@ "@types/node": "^20.14.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react-swc": "^3.5.0", + "@vitejs/plugin-react-swc": "^4.3.2", "autoprefixer": "^10.4.20", "electron": "^42.0.0", - "electron-vite": "^5.0.0", + "electron-vite": "^6.0.0-beta.1", "eslint": "^9.15.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.4.0", @@ -45,6 +45,10 @@ "tailwindcss": "^3.4.16", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", - "vite": "^6.4.2" + "vite": "^8.0.10" + }, + "overrides": { + "undici": "7.28.0", + "esbuild": "0.28.1" } } diff --git a/electron/src/main/ditto-manager.ts b/electron/src/main/ditto-manager.ts new file mode 100644 index 000000000..3325a5889 --- /dev/null +++ b/electron/src/main/ditto-manager.ts @@ -0,0 +1,70 @@ +import { Authenticator, Ditto, DittoConfig } from '@dittolive/ditto'; +import { app } from 'electron'; +import { env } from './env'; +import type { DittoIdentity } from '../types'; + +// Owns the Ditto instance lifecycle: open, identity/auth, transport config, +// and sync start/stop. Knows nothing about tasks. +export class DittoManager { + private ditto: Ditto | null = null; + + async open(): Promise { + const config = new DittoConfig( + env.databaseId, + { mode: 'server', url: env.serverUrl }, + app.getPath('userData'), + ); + + const ditto = await Ditto.open(config); + this.ditto = ditto; + + // Authenticate with the development token, and re-authenticate when it expires. + await ditto.auth.setExpirationHandler(async (instance) => { + const result = await instance.auth.login( + env.token, + Authenticator.DEVELOPMENT_PROVIDER, + ); + if (result.error) { + console.error('Re-authentication failed:', result.error); + } + }); + + // BLE and AWDL require macOS entitlements that only signed app bundles get, + // so they're disabled here for unsigned `npm run dev` builds. LAN (TCP + + // mDNS) provides peer-to-peer sync across the local network. + ditto.updateTransportConfig((cfg) => { + cfg.peerToPeer.bluetoothLE.isEnabled = false; + cfg.peerToPeer.awdl.isEnabled = false; + cfg.peerToPeer.lan.isEnabled = true; + cfg.peerToPeer.lan.isMdnsEnabled = true; + cfg.peerToPeer.lan.isMulticastEnabled = true; + }); + + ditto.sync.start(); + + return ditto; + } + + getInfo(): DittoIdentity { + return { databaseId: env.databaseId, token: env.token }; + } + + startSync(): void { + this.ditto?.sync.start(); + } + + stopSync(): void { + this.ditto?.sync.stop(); + } + + close(): void { + if (this.ditto) { + try { + this.ditto.close(); + } catch (e) { + console.error('Error closing Ditto:', e); + } + this.ditto = null; + } + } +} diff --git a/electron/src/main/ditto.ts b/electron/src/main/ditto.ts deleted file mode 100644 index 330dc65ff..000000000 --- a/electron/src/main/ditto.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - Authenticator, - Ditto, - DittoConfig, - StoreObserver, - SyncSubscription, -} from '@dittolive/ditto'; -import { app } from 'electron'; -import { env } from './env'; -import type { DittoIdentity, Task } from '../types'; - -let ditto: Ditto | null = null; -let subscription: SyncSubscription | null = null; -let observer: StoreObserver | null = null; -let currentTasks: Task[] = []; - -export async function initDitto( - onTasksUpdated: (tasks: Task[]) => void, -): Promise { - const config = new DittoConfig( - env.appId, - { mode: 'server', url: env.authUrl }, - app.getPath('userData'), - ); - - ditto = await Ditto.open(config); - - // Authenticate with the playground token, and re-authenticate when it expires. - await ditto.auth.setExpirationHandler(async (instance) => { - const result = await instance.auth.login( - env.token, - Authenticator.DEVELOPMENT_PROVIDER, - ); - if (result.error) { - console.error('Re-authentication failed:', result.error); - } - }); - - // BLE and AWDL require macOS entitlements that only signed app bundles get, - // so they're disabled here for unsigned `npm run dev` builds. LAN (TCP + - // mDNS) provides peer-to-peer sync across the local network; the websocket - // URL provides cloud sync to Ditto's Big Peer. - ditto.updateTransportConfig((cfg) => { - cfg.connect.websocketURLs = [env.websocketUrl]; - cfg.peerToPeer.bluetoothLE.isEnabled = false; - cfg.peerToPeer.awdl.isEnabled = false; - cfg.peerToPeer.lan.isEnabled = true; - cfg.peerToPeer.lan.isMdnsEnabled = true; - cfg.peerToPeer.lan.isMulticastEnabled = true; - }); - - ditto.sync.start(); - - // https://docs.ditto.com/sdk/latest/sync/syncing-data#creating-subscriptions - subscription = ditto.sync.registerSubscription('SELECT * FROM tasks'); - - // https://docs.ditto.com/sdk/latest/crud/observing-data-changes#setting-up-store-observers - observer = ditto.store.registerObserver( - 'SELECT * FROM tasks WHERE deleted=false ORDER BY title ASC', - (results) => { - currentTasks = results.items.map((item) => item.value); - onTasksUpdated(currentTasks); - }, - ); -} - -export function getInfo(): DittoIdentity { - return { appId: env.appId, token: env.token }; -} - -// Returns the latest observed task list. Lets the renderer hydrate its -// initial state after registering its onTasksUpdated listener, closing -// the race where the observer may have fired during cloud sync before -// the BrowserWindow was ready to receive IPC events. -export function getTasks(): Task[] { - return currentTasks; -} - -// https://docs.ditto.com/sdk/latest/crud/create -export async function createTask(title: string): Promise { - await ditto?.store.execute('INSERT INTO tasks DOCUMENTS (:task)', { - task: { title, done: false, deleted: false }, - }); -} - -// https://docs.ditto.com/sdk/latest/crud/update -export async function editTask(id: string, title: string): Promise { - await ditto?.store.execute('UPDATE tasks SET title=:title WHERE _id=:id', { - id, - title, - }); -} - -export async function toggleTask(id: string, done: boolean): Promise { - await ditto?.store.execute('UPDATE tasks SET done=:done WHERE _id=:id', { - id, - done, - }); -} - -// https://docs.ditto.com/sdk/latest/crud/delete#soft-delete-pattern -export async function deleteTask(id: string): Promise { - await ditto?.store.execute('UPDATE tasks SET deleted=true WHERE _id=:id', { - id, - }); -} - -export function startSync(): void { - ditto?.sync.start(); -} - -export function stopSync(): void { - ditto?.sync.stop(); -} - -export async function shutdown(): Promise { - observer?.cancel(); - subscription?.cancel(); - observer = null; - subscription = null; - currentTasks = []; - if (ditto) { - try { - ditto.close(); - } catch (e) { - console.error('Error closing Ditto:', e); - } - ditto = null; - } -} diff --git a/electron/src/main/env.ts b/electron/src/main/env.ts index 4b3778ffd..07d1dfb53 100644 --- a/electron/src/main/env.ts +++ b/electron/src/main/env.ts @@ -4,21 +4,20 @@ import { resolve } from 'node:path'; dotenv.config({ path: resolve(process.cwd(), '..', '.env') }); export const env = { - appId: process.env.DITTO_APP_ID ?? '', - token: process.env.DITTO_PLAYGROUND_TOKEN ?? '', - authUrl: process.env.DITTO_AUTH_URL ?? '', - websocketUrl: process.env.DITTO_WEBSOCKET_URL ?? '', + databaseId: process.env.DITTO_DATABASE_ID ?? '', + token: process.env.DITTO_DEVELOPMENT_TOKEN ?? '', + serverUrl: process.env.DITTO_SERVER_URL ?? '', }; export function assertEnv(): void { - const missing = ( - ['appId', 'token', 'authUrl', 'websocketUrl'] as const - ).filter((k) => !env[k]); + const missing = (['databaseId', 'token', 'serverUrl'] as const).filter( + (k) => !env[k], + ); if (missing.length > 0) { throw new Error( `Missing required env vars: ${missing.join(', ')}. ` + - `Copy .env.sample to .env at the repo root and fill in DITTO_APP_ID, ` + - `DITTO_PLAYGROUND_TOKEN, DITTO_AUTH_URL, DITTO_WEBSOCKET_URL.`, + `Copy .env.sample to .env at the repo root and fill in DITTO_DATABASE_ID, ` + + `DITTO_DEVELOPMENT_TOKEN, DITTO_SERVER_URL.`, ); } } diff --git a/electron/src/main/index.ts b/electron/src/main/index.ts index 44e07a0b5..14c986643 100644 --- a/electron/src/main/index.ts +++ b/electron/src/main/index.ts @@ -2,23 +2,22 @@ import { app, BrowserWindow, ipcMain } from 'electron'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { assertEnv } from './env'; -import { - createTask, - deleteTask, - editTask, - getInfo, - getTasks, - initDitto, - shutdown, - startSync, - stopSync, - toggleTask, -} from './ditto'; +import { DittoManager } from './ditto-manager'; +import { TasksRepository } from './tasks-repository'; import { IPC } from '../types'; import type { Task } from '../types'; const __dirname = dirname(fileURLToPath(import.meta.url)); +const manager = new DittoManager(); +let repository: TasksRepository | null = null; + +async function shutdown(): Promise { + repository?.dispose(); + repository = null; + manager.close(); +} + function broadcastTasks(tasks: Task[]): void { for (const win of BrowserWindow.getAllWindows()) { win.webContents.send(IPC.TASKS_UPDATED, tasks); @@ -47,23 +46,29 @@ function createWindow(): BrowserWindow { return win; } -ipcMain.handle(IPC.TASKS_GET_INFO, () => getInfo()); -ipcMain.handle(IPC.TASKS_GET, () => getTasks()); -ipcMain.handle(IPC.TASKS_CREATE, (_event, title: string) => createTask(title)); +ipcMain.handle(IPC.TASKS_GET_INFO, () => manager.getInfo()); +ipcMain.handle(IPC.TASKS_GET, () => repository?.getTasks() ?? []); +ipcMain.handle(IPC.TASKS_CREATE, (_event, title: string) => + repository?.createTask(title), +); ipcMain.handle(IPC.TASKS_EDIT, (_event, id: string, title: string) => - editTask(id, title), + repository?.editTask(id, title), ); ipcMain.handle(IPC.TASKS_TOGGLE, (_event, id: string, done: boolean) => - toggleTask(id, done), + repository?.toggleTask(id, done), +); +ipcMain.handle(IPC.TASKS_DELETE, (_event, id: string) => + repository?.deleteTask(id), ); -ipcMain.handle(IPC.TASKS_DELETE, (_event, id: string) => deleteTask(id)); -ipcMain.handle(IPC.TASKS_START_SYNC, () => startSync()); -ipcMain.handle(IPC.TASKS_STOP_SYNC, () => stopSync()); +ipcMain.handle(IPC.TASKS_START_SYNC, () => manager.startSync()); +ipcMain.handle(IPC.TASKS_STOP_SYNC, () => manager.stopSync()); app.whenReady().then(async () => { try { assertEnv(); - await initDitto(broadcastTasks); + const ditto = await manager.open(); + repository = new TasksRepository(ditto, broadcastTasks); + repository.start(); } catch (e) { console.error('Startup failed:', e); app.quit(); diff --git a/electron/src/main/tasks-repository.ts b/electron/src/main/tasks-repository.ts new file mode 100644 index 000000000..775a2d478 --- /dev/null +++ b/electron/src/main/tasks-repository.ts @@ -0,0 +1,78 @@ +import { Ditto, StoreObserver, SyncSubscription } from '@dittolive/ditto'; +import type { Task } from '../types'; + +// Owns the tasks concern against a ready Ditto instance: registers the +// subscription and observer, holds the latest observed task list, and +// exposes CRUD. Calls the Ditto API directly. +export class TasksRepository { + private subscription: SyncSubscription | null = null; + private observer: StoreObserver | null = null; + private currentTasks: Task[] = []; + + constructor( + private readonly ditto: Ditto, + private readonly onTasksUpdated: (tasks: Task[]) => void, + ) {} + + start(): void { + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + this.subscription = this.ditto.sync.registerSubscription( + 'SELECT * FROM tasks', + ); + + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + this.observer = this.ditto.store.registerObserver( + 'SELECT * FROM tasks WHERE NOT deleted', + (results) => { + this.currentTasks = results.items.map((item) => item.value); + this.onTasksUpdated(this.currentTasks); + }, + ); + } + + // Returns the latest observed task list. Lets the renderer hydrate its + // initial state after registering its onTasksUpdated listener, closing + // the race where the observer may have fired during sync before + // the BrowserWindow was ready to receive IPC events. + getTasks(): Task[] { + return this.currentTasks; + } + + // https://docs.ditto.live/sdk/latest/crud/create + async createTask(title: string): Promise { + await this.ditto.store.execute('INSERT INTO tasks DOCUMENTS (:task)', { + task: { title, done: false, deleted: false }, + }); + } + + // https://docs.ditto.live/sdk/latest/crud/update + async editTask(id: string, title: string): Promise { + await this.ditto.store.execute( + 'UPDATE tasks SET title = :title WHERE _id = :id', + { id, title }, + ); + } + + async toggleTask(id: string, done: boolean): Promise { + await this.ditto.store.execute( + 'UPDATE tasks SET done = :done WHERE _id = :id', + { id, done }, + ); + } + + // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern + async deleteTask(id: string): Promise { + await this.ditto.store.execute( + 'UPDATE tasks SET deleted = true WHERE _id = :id', + { id }, + ); + } + + dispose(): void { + this.observer?.cancel(); + this.subscription?.cancel(); + this.observer = null; + this.subscription = null; + this.currentTasks = []; + } +} diff --git a/electron/src/renderer/App.tsx b/electron/src/renderer/App.tsx index bedaa5c72..424bb621b 100644 --- a/electron/src/renderer/App.tsx +++ b/electron/src/renderer/App.tsx @@ -6,7 +6,7 @@ import type { Task } from '../types'; const App = () => { const [error, setError] = useState(null); - const [appId, setAppId] = useState(''); + const [databaseId, setDatabaseId] = useState(''); const [token, setToken] = useState(''); const [syncActive, setSyncActive] = useState(true); const [isInitialized, setIsInitialized] = useState(false); @@ -20,7 +20,7 @@ const App = () => { try { const info = await window.ditto.getInfo(); if (!isMounted) return; - setAppId(info.appId); + setDatabaseId(info.databaseId); setToken(info.token); // Subscribe before fetching initial state so updates that arrive // during the round-trip aren't missed. @@ -53,7 +53,7 @@ const App = () => { }); }, []); - // https://docs.ditto.com/sdk/latest/crud/create + // https://docs.ditto.live/sdk/latest/crud/create const createTask = useCallback(async (title: string) => { try { await window.ditto.createTask(title); @@ -62,7 +62,7 @@ const App = () => { } }, []); - // https://docs.ditto.com/sdk/latest/crud/update + // https://docs.ditto.live/sdk/latest/crud/update const editTask = useCallback(async (id: string, title: string) => { try { await window.ditto.editTask(id, title); @@ -79,7 +79,7 @@ const App = () => { } }, []); - // https://docs.ditto.com/sdk/latest/crud/delete#soft-delete-pattern + // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern const deleteTask = useCallback(async (task: Task) => { try { await window.ditto.deleteTask(task._id); @@ -93,7 +93,7 @@ const App = () => {
{error && } void; @@ -9,7 +9,7 @@ type Props = { }; const DittoInfo: React.FC = ({ - appId, + databaseId, token, syncEnabled, onToggleSync, @@ -21,7 +21,7 @@ const DittoInfo: React.FC = ({ Ditto Tasks
-

App ID: {appId}

+

Database ID: {databaseId}

Token: {token}

If you haven't created a Ditto Portal account, you can sign up at [Ditto Portal](https://portal.ditto.live) or checkout [this video](https://www.youtube.com/watch?v=1aLiDkgl0Dc) on how to setup a Ditto Portal account. > -#### Obtain App ID and Playground Token +#### Obtain Database ID and Development Token - Log in to your Ditto Portal account -- Navigate to your application and obtain the App ID and Playground Token (see [Sync Credentials](https://docs.ditto.live/cloud/portal/getting-sdk-connection-details) +- Navigate to your application and obtain the Database ID and Development Token (see [Sync Credentials](https://docs.ditto.live/cloud/portal/getting-sdk-connection-details) for more details) -Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and put it into the flutter_app directory (same directory as pubspec.yaml) and add your app ID, online playground token, authUrl and websocket URL. The app will crash if you don't provide an .env file. +Copy the `.env.sample` file at the top level of the quickstart repo to `.env` and put it into the flutter_app directory (same directory as pubspec.yaml) and add your Database ID, development token, and serverUrl. The app will crash if you don't provide an .env file. ### 4. Run the Application diff --git a/flutter_app/android/build.gradle b/flutter_app/android/build.gradle index 0c47cbc0a..08282f487 100644 --- a/flutter_app/android/build.gradle +++ b/flutter_app/android/build.gradle @@ -20,6 +20,12 @@ subprojects { // breaks the manifest merger. Bump every Android subproject (notably // :ditto_live) to match. subprojects { + // :app sets its own minSdkVersion and is already evaluated (forced by + // evaluationDependsOn above); registering afterEvaluate on an already- + // evaluated project throws in Gradle 8.10. Skip it here — only plugin + // library subprojects (e.g. :ditto_live) need the bump. + if (project.name == 'app') return + afterEvaluate { project -> if (project.plugins.hasPlugin('com.android.application') || project.plugins.hasPlugin('com.android.library')) { diff --git a/flutter_app/android/gradlew.bat b/flutter_app/android/gradlew.bat index 9b42019c7..9d21a2183 100755 --- a/flutter_app/android/gradlew.bat +++ b/flutter_app/android/gradlew.bat @@ -1,94 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/flutter_app/integration_test/app_test.dart b/flutter_app/integration_test/app_test.dart index 4adacedf6..ca3aa2a25 100644 --- a/flutter_app/integration_test/app_test.dart +++ b/flutter_app/integration_test/app_test.dart @@ -13,8 +13,9 @@ void main() { await dotenv.load(fileName: ".env"); }); - testWidgets('App loads and syncs with Ditto Cloud', - (WidgetTester tester) async { + testWidgets('App loads and syncs with Ditto server', ( + WidgetTester tester, + ) async { // Initialize app await app.main(); // Allow up to 10 seconds for Ditto to initialise and the first sync @@ -36,20 +37,19 @@ void main() { // Verify add task FAB is present expect(find.byIcon(Icons.add_task), findsOneWidget); - // Verify clear button is present - expect(find.byIcon(Icons.clear), findsOneWidget); - - // Look for the test document that should be synced from Ditto cloud. - // The playground can accumulate many documents from previous CI runs, + // Look for the test document that should be synced from Ditto server. + // The Ditto server can accumulate many documents from previous CI runs, // so we poll rather than waiting a fixed amount of time. const testTitle = String.fromEnvironment('TASK_TO_FIND'); if (testTitle.isEmpty) { - throw Exception('TASK_TO_FIND environment variable must be set. ' - 'Build with: --dart-define=TASK_TO_FIND='); + throw Exception( + 'TASK_TO_FIND environment variable must be set. ' + 'Build with: --dart-define=TASK_TO_FIND=', + ); } - // Poll every 500 ms for up to 45 seconds to give the cloud sync + // Poll every 500 ms for up to 45 seconds to give the sync // enough time to deliver and write all documents to the local store. const syncTimeout = Duration(seconds: 45); final deadline = DateTime.now().add(syncTimeout); @@ -63,9 +63,12 @@ void main() { } } - expect(taskFound, isTrue, - reason: - 'Should find test document with title: $testTitle synced from Ditto cloud within ${syncTimeout.inSeconds}s'); + expect( + taskFound, + isTrue, + reason: + 'Should find test document with title: $testTitle synced from Ditto server within ${syncTimeout.inSeconds}s', + ); }); }); } diff --git a/flutter_app/ios/Podfile.lock b/flutter_app/ios/Podfile.lock index cd68bce69..b657e3ea9 100644 --- a/flutter_app/ios/Podfile.lock +++ b/flutter_app/ios/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (5.0.0): - - DittoFlutter (= 5.0.0) + - ditto_live (5.0.3): + - DittoFlutter (= 5.0.3) - Flutter - - DittoFlutter (5.0.0) + - DittoFlutter (5.0.3) - Flutter (1.0.0) - integration_test (0.0.1): - Flutter @@ -36,9 +36,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" SPEC CHECKSUMS: - ditto_live: f038d915f2ad9206d180d3fd8b231d149b71e3f2 - DittoFlutter: 90c4bcdf128b0a14bf8f6f923369e7a6ddccd27d - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + ditto_live: 16373b211bdc6534b7f45aa17f4632f47dc1bcc6 + DittoFlutter: 411f743ae08d4802a69c508a055aecfde62a51dc + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d diff --git a/flutter_app/lib/dialog.dart b/flutter_app/lib/dialog.dart index 09ba560ae..b32da35ac 100644 --- a/flutter_app/lib/dialog.dart +++ b/flutter_app/lib/dialog.dart @@ -3,10 +3,7 @@ import 'package:flutter/material.dart'; import 'task.dart'; Future showAddTaskDialog(BuildContext context, [Task? task]) => - showDialog( - context: context, - builder: (context) => _Dialog(task), - ); + showDialog(context: context, builder: (context) => _Dialog(task)); class _Dialog extends StatefulWidget { final Task? taskToEdit; @@ -34,10 +31,7 @@ class _DialogState extends State<_Dialog> { content: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, - children: [ - _textInput(_name, "Name"), - _doneSwitch, - ], + children: [_textInput(_name, "Name"), _doneSwitch], ), actions: [ TextButton( @@ -47,11 +41,7 @@ class _DialogState extends State<_Dialog> { ElevatedButton( child: Text(widget.taskToEdit == null ? "Add Task" : "Edit Task"), onPressed: () { - final task = Task( - title: _name.text, - done: _done, - deleted: false, - ); + final task = Task(title: _name.text, done: _done, deleted: false); Navigator.of(context).pop(task); }, ), @@ -61,9 +51,7 @@ class _DialogState extends State<_Dialog> { Widget _textInput(TextEditingController controller, String label) => ListTile( title: TextField( controller: controller, - decoration: InputDecoration( - labelText: label, - ), + decoration: InputDecoration(labelText: label), ), ); diff --git a/flutter_app/lib/ditto_manager.dart b/flutter_app/lib/ditto_manager.dart new file mode 100644 index 000000000..5a93dca4e --- /dev/null +++ b/flutter_app/lib/ditto_manager.dart @@ -0,0 +1,86 @@ +import 'dart:io' show Platform; + +import 'package:ditto_live/ditto_live.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:permission_handler/permission_handler.dart'; + +/// Owns the Ditto instance lifecycle: permissions, config, open, +/// identity/auth, and sync start/stop. It vends a configured, already-running +/// Ditto instance for the rest of the app to use and knows nothing about tasks. +class DittoManager { + final String databaseId = + dotenv.env['DITTO_DATABASE_ID'] ?? (throw Exception("env not found")); + final String token = dotenv.env['DITTO_DEVELOPMENT_TOKEN'] ?? + (throw Exception("env not found")); + final String serverUrl = + dotenv.env['DITTO_SERVER_URL'] ?? (throw Exception("env not found")); + + Ditto? _ditto; + + /// The ready Ditto instance. Only valid after [open] has completed. + Ditto get ditto => _ditto!; + + bool get isSyncActive => ditto.sync.isActive; + + /// Initializes the Ditto instance with necessary permissions and configuration. + /// https://docs.ditto.live/sdk/latest/install-guides/flutter#step-3-import-and-initialize-the-ditto-sdk + /// + /// This function: + /// 1. Requests required Bluetooth and WiFi permissions on mobile platforms (Android/iOS) + /// 2. Initializes the Ditto SDK + /// 3. Sets up development identity with the provided database ID and token + /// 4. Enables peer-to-peer communication on non-web platforms + /// 5. Configures the connection to the Ditto server + /// 6. Starts sync and returns the configured Ditto instance + Future open() async { + // Skip permissions in test mode - they block integration tests + const isTestMode = bool.fromEnvironment( + 'INTEGRATION_TEST_MODE', + defaultValue: false, + ); + + // Only request permissions on mobile platforms (Android/iOS) + // Desktop platforms (macOS, Windows, Linux) don't require these permissions + final isMobilePlatform = !kIsWeb && (Platform.isAndroid || Platform.isIOS); + if (isMobilePlatform && !isTestMode) { + await [ + Permission.bluetoothConnect, + Permission.bluetoothAdvertise, + Permission.nearbyWifiDevices, + Permission.bluetoothScan, + ].request(); + } + + await Ditto.init(); + + DittoLogger.isEnabled = true; + DittoLogger.minimumLogLevel = LogLevel.debug; + + //new configuration - https://docs.ditto.live/sdk/latest/ditto-config + final config = DittoConfig( + databaseID: databaseId, + connect: DittoConfigConnectServer(url: serverUrl), + ); + final ditto = await Ditto.open(config); + _ditto = ditto; + + await ditto.auth.setExpirationHandler((ditto, timeUntilExpiration) async { + final authResult = await ditto.auth.login( + token: token, + provider: Authenticator.developmentProvider, + ); + if (authResult.exception != null) { + throw authResult.exception!; + } + }); + + ditto.sync.start(); + + return ditto; + } + + void startSync() => ditto.sync.start(); + + void stopSync() => ditto.sync.stop(); +} diff --git a/flutter_app/lib/dql_builder.dart b/flutter_app/lib/dql_builder.dart deleted file mode 100644 index 1240dc1f2..000000000 --- a/flutter_app/lib/dql_builder.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:ditto_live/ditto_live.dart'; -import 'package:flutter/material.dart'; - -class DqlBuilder extends StatefulWidget { - final Ditto ditto; - final String query; - final Map? queryArgs; - final Widget Function(BuildContext, QueryResult) builder; - final Widget? loading; - - const DqlBuilder({ - super.key, - required this.ditto, - required this.query, - this.queryArgs, - required this.builder, - this.loading, - }); - - @override - State createState() => _DqlBuilderState(); -} - -class _DqlBuilderState extends State { - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes - StoreObserver? _observer; - - // https://docs.ditto.live/sdk/latest/sync/syncing-data - SyncSubscription? _subscription; - - @override - void initState() { - super.initState(); - - // Register observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - final observer = widget.ditto.store.registerObserver( - widget.query, - arguments: widget.queryArgs ?? {}, - ); - - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - final subscription = widget.ditto.sync.registerSubscription( - widget.query, - arguments: widget.queryArgs ?? {}, - ); - - setState(() { - _observer = observer; - _subscription = subscription; - }); - } - - @override - void didUpdateWidget(covariant DqlBuilder oldWidget) { - super.didUpdateWidget(oldWidget); - - final isSame = widget.query == oldWidget.query && - widget.queryArgs == oldWidget.queryArgs; - - if (!isSame) { - _observer?.cancel(); - _subscription?.cancel(); - - // Register observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - final observer = widget.ditto.store.registerObserver( - widget.query, - arguments: widget.queryArgs ?? {}, - ); - - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - final subscription = widget.ditto.sync.registerSubscription( - widget.query, - arguments: widget.queryArgs ?? {}, - ); - - setState(() { - _observer = observer; - _subscription = subscription; - }); - } - } - - @override - void dispose() { - _observer?.cancel(); - _subscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final placeholder = widget.loading ?? _defaultLoading; - final stream = _observer?.changes; - if (stream == null) return placeholder; - - return StreamBuilder( - stream: stream, - builder: (context, snapshot) { - final response = snapshot.data; - if (response == null) return widget.loading ?? _defaultLoading; - return widget.builder(context, response); - }); - } -} - -const _defaultLoading = Center(child: CircularProgressIndicator()); diff --git a/flutter_app/lib/main.dart b/flutter_app/lib/main.dart index e1ec06f22..41ef837e7 100644 --- a/flutter_app/lib/main.dart +++ b/flutter_app/lib/main.dart @@ -1,12 +1,9 @@ -import 'dart:io' show Platform; - import 'package:ditto_live/ditto_live.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_quickstart/dialog.dart'; -import 'package:flutter_quickstart/dql_builder.dart'; +import 'package:flutter_quickstart/ditto_manager.dart'; +import 'package:flutter_quickstart/tasks_repository.dart'; import 'package:flutter_quickstart/task.dart'; import 'package:flutter/material.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; Future main() async { @@ -24,112 +21,49 @@ class DittoExample extends StatefulWidget { } class _DittoExampleState extends State { - Ditto? _ditto; - final appID = - dotenv.env['DITTO_APP_ID'] ?? (throw Exception("env not found")); - final token = dotenv.env['DITTO_PLAYGROUND_TOKEN'] ?? - (throw Exception("env not found")); - final authUrl = - dotenv.env['DITTO_AUTH_URL'] ?? (throw Exception("env not found")); + final DittoManager _manager = DittoManager(); + TasksRepository? _repository; @override void initState() { super.initState(); - _initDitto(); + _init(); } - /// Initializes the Ditto instance with necessary permissions and configuration. - /// https://docs.ditto.live/sdk/latest/install-guides/flutter#step-3-import-and-initialize-the-ditto-sdk - /// - /// This function: - /// 1. Requests required Bluetooth and WiFi permissions on mobile platforms (Android/iOS) - /// 2. Initializes the Ditto SDK - /// 3. Sets up online playground identity with the provided app ID and token - /// 4. Enables peer-to-peer communication on non-web platforms - /// 5. Configures WebSocket connection to Ditto cloud - /// 6. Disables DQL strict mode - /// 7. Starts sync and updates the app state with the configured Ditto instance - Future _initDitto() async { - // Skip permissions in test mode - they block integration tests - const isTestMode = - bool.fromEnvironment('INTEGRATION_TEST_MODE', defaultValue: false); - - // Only request permissions on mobile platforms (Android/iOS) - // Desktop platforms (macOS, Windows, Linux) don't require these permissions - final isMobilePlatform = !kIsWeb && (Platform.isAndroid || Platform.isIOS); - if (isMobilePlatform && !isTestMode) { - await [ - Permission.bluetoothConnect, - Permission.bluetoothAdvertise, - Permission.nearbyWifiDevices, - Permission.bluetoothScan - ].request(); - } + Future _init() async { + // The manager vends a configured, already-running Ditto instance; the + // repository then registers the tasks subscription and observer against it. + await _manager.open(); - await Ditto.init(); - - DittoLogger.isEnabled = true; - DittoLogger.minimumLogLevel = LogLevel.debug; - - //new configuration - https://docs.ditto.live/sdk/latest/ditto-config - final config = DittoConfig( - databaseID: appID, connect: DittoConfigConnectServer(url: authUrl)); - final ditto = await Ditto.open(config); - await ditto.auth.setExpirationHandler((ditto, timeUntilExpiration) async { - final authResult = await ditto.auth - .login(token: token, provider: Authenticator.developmentProvider); - if (authResult.exception != null) { - throw authResult.exception!; - } - }); - - // Register the tasks subscription before starting sync so it is - // included in the very first sync exchange with the cloud. - // Without this, the subscription is registered later when the - // DqlBuilder widget builds, causing a 5–10 second delay on the - // first sync cycle. - ditto.sync.registerSubscription( - "SELECT * FROM tasks WHERE deleted = false", - ); - - ditto.sync.start(); + final repository = TasksRepository(_manager.ditto); + repository.start(); if (mounted) { - setState(() => _ditto = ditto); + setState(() => _repository = repository); } } + @override + void dispose() { + _repository?.dispose(); + super.dispose(); + } + Future _addTask() async { final task = await showAddTaskDialog(context); if (task == null) return; - // https://docs.ditto.live/sdk/latest/crud/create - await _ditto!.store.execute( - "INSERT INTO tasks DOCUMENTS (:task)", - arguments: {"task": task.toJson()}, - ); - } - - Future _clearTasks() async { - // https://docs.ditto.live/sdk/latest/crud/delete#evicting-data - await _ditto!.store.execute("EVICT FROM tasks WHERE true"); + await _repository!.addTask(task.toJson()); } @override Widget build(BuildContext context) { - if (_ditto == null) return _loading; + if (_repository == null) return _loading; return Scaffold( appBar: AppBar( title: const Text("Ditto Tasks"), - actions: [ - IconButton( - icon: const Icon(Icons.clear), - tooltip: "Clear", - onPressed: _clearTasks, - ), - ], ), floatingActionButton: _fab, body: Column( @@ -152,8 +86,8 @@ class _DittoExampleState extends State { CrossAxisAlignment.center, // Center horizontally children: [ const CircularProgressIndicator(), - const Text("Ensure your AppID and Token are correct"), - _portalInfo + const Text("Ensure your Database ID and Token are correct"), + _portalInfo, ], ), ), @@ -164,51 +98,47 @@ class _DittoExampleState extends State { child: const Icon(Icons.add_task), ); - Widget get _portalInfo => Column(children: [ - Text("AppID: $appID"), - Text("Token: $token"), - ]); + Widget get _portalInfo => Column( + children: [ + Text("Database ID: ${_manager.databaseId}"), + Text("Token: ${_manager.token}"), + ], + ); Widget get _syncTile => SwitchListTile( title: const Text("Sync Active"), - value: _ditto!.sync.isActive, + value: _manager.isSyncActive, onChanged: (value) { if (value) { - setState(() => _ditto!.sync.start()); + setState(() => _manager.startSync()); } else { - setState(() => _ditto!.sync.stop()); + setState(() => _manager.stopSync()); } }, ); - // Ordering is intentionally omitted here because this screen currently uses - // a single DqlBuilder query, and reintroducing title-based ordering would - // require a different query approach. - Widget get _tasksList => DqlBuilder( - ditto: _ditto!, - query: "SELECT * FROM tasks WHERE deleted = false", - builder: (context, result) { + Widget get _tasksList => StreamBuilder( + stream: _repository!.changes, + builder: (context, snapshot) { + final result = snapshot.data; + if (result == null) { + return const Center(child: CircularProgressIndicator()); + } final tasks = result.items.map((r) => r.value).map(Task.fromJson); - return ListView( - children: tasks.map(_singleTask).toList(), - ); + return ListView(children: tasks.map(_singleTask).toList()); }, ); Widget _singleTask(Task task) => Dismissible( key: Key("${task.id}-${task.title}"), onDismissed: (direction) async { - // Use the Soft-Delete pattern - // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern - await _ditto!.store.execute( - "UPDATE tasks SET deleted = true WHERE _id = :id", - arguments: {"id": task.id}, - ); + await _repository!.deleteTask(task.id!); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Deleted Task ${task.title}")), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar(content: Text("Deleted Task ${task.title}"))); } }, background: _dismissibleBackground(true), @@ -216,10 +146,7 @@ class _DittoExampleState extends State { child: CheckboxListTile( title: Text(task.title), value: task.done, - onChanged: (value) => _ditto!.store.execute( - "UPDATE tasks SET done = :done WHERE _id = :id", - arguments: {"done": value, "id": task.id}, - ), + onChanged: (value) => _repository!.setDone(task.id!, value ?? false), secondary: IconButton( icon: const Icon(Icons.edit), tooltip: "Edit Task", @@ -227,11 +154,7 @@ class _DittoExampleState extends State { final newTask = await showAddTaskDialog(context, task); if (newTask == null) return; - // https://docs.ditto.live/sdk/latest/crud/update - _ditto!.store.execute( - "UPDATE tasks SET title = :title WHERE _id = :id", - arguments: {"title": newTask.title, "id": task.id}, - ); + await _repository!.updateTitle(task.id!, newTask.title); }, ), ), diff --git a/flutter_app/lib/tasks_repository.dart b/flutter_app/lib/tasks_repository.dart new file mode 100644 index 000000000..eca07f013 --- /dev/null +++ b/flutter_app/lib/tasks_repository.dart @@ -0,0 +1,79 @@ +import 'package:ditto_live/ditto_live.dart'; + +/// Owns the tasks concern against a ready Ditto instance: registers the tasks +/// subscription and observer, exposes the observed task results as a stream, +/// and provides CRUD. It calls the real Ditto API directly. +class TasksRepository { + final Ditto _ditto; + + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes + StoreObserver? _observer; + + // https://docs.ditto.live/sdk/latest/sync/syncing-data + SyncSubscription? _subscription; + + TasksRepository(this._ditto); + + /// Registers the subscription (what syncs to this peer) and the observer + /// (what is read from the local database). The subscription is a superset of + /// the observer query so it syncs every task, while the observer filters out + /// soft-deleted tasks for display. + void start() { + // Register a subscription, which determines what data syncs to this peer + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + _subscription = _ditto.sync.registerSubscription( + "SELECT * FROM tasks", + ); + + // Register observer, which runs against the local database on this peer + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + _observer = _ditto.store.registerObserver( + "SELECT * FROM tasks WHERE NOT deleted", + ); + } + + /// Stream of query results for the observed tasks. Emits whenever the + /// matching set of tasks in the local store changes. + Stream? get changes => _observer?.changes; + + // https://docs.ditto.live/sdk/latest/crud/create + Future addTask(Map task) async { + await _ditto.store.execute( + "INSERT INTO tasks DOCUMENTS (:task)", + arguments: {"task": task}, + ); + } + + // https://docs.ditto.live/sdk/latest/crud/update + Future setDone(String id, bool done) async { + await _ditto.store.execute( + "UPDATE tasks SET done = :done WHERE _id = :id", + arguments: {"done": done, "id": id}, + ); + } + + // https://docs.ditto.live/sdk/latest/crud/update + Future updateTitle(String id, String title) async { + await _ditto.store.execute( + "UPDATE tasks SET title = :title WHERE _id = :id", + arguments: {"title": title, "id": id}, + ); + } + + // Use the Soft-Delete pattern + // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern + Future deleteTask(String id) async { + await _ditto.store.execute( + "UPDATE tasks SET deleted = true WHERE _id = :id", + arguments: {"id": id}, + ); + } + + /// Cancel the subscription and observer on teardown. + void dispose() { + _observer?.cancel(); + _subscription?.cancel(); + _observer = null; + _subscription = null; + } +} diff --git a/flutter_app/macos/Podfile.lock b/flutter_app/macos/Podfile.lock index f1c633448..5456b1242 100644 --- a/flutter_app/macos/Podfile.lock +++ b/flutter_app/macos/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (5.0.0): - - DittoFlutter (= 5.0.0) + - ditto_live (5.0.3): + - DittoFlutter (= 5.0.3) - FlutterMacOS - - DittoFlutter (5.0.0) + - DittoFlutter (5.0.3) - FlutterMacOS (1.0.0) - path_provider_foundation (0.0.1): - Flutter @@ -26,9 +26,9 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin SPEC CHECKSUMS: - ditto_live: 38b290325b127be4157b8de4c2105c9d0e1371e9 - DittoFlutter: 90c4bcdf128b0a14bf8f6f923369e7a6ddccd27d - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + ditto_live: 7a7a743bf633abefcd67d1f7ad1e7f9b21e33417 + DittoFlutter: 411f743ae08d4802a69c508a055aecfde62a51dc + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PODFILE CHECKSUM: 1e95c36afbfd1cb6423ceca4de7a8e1b256fb6ac diff --git a/flutter_app/macos/Runner.xcodeproj/project.pbxproj b/flutter_app/macos/Runner.xcodeproj/project.pbxproj index a28d8fdb8..057b8bea8 100644 --- a/flutter_app/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter_app/macos/Runner.xcodeproj/project.pbxproj @@ -557,7 +557,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -639,7 +639,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -689,7 +689,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/flutter_app/pubspec.lock b/flutter_app/pubspec.lock index 4b2ac731f..090babb4e 100644 --- a/flutter_app/pubspec.lock +++ b/flutter_app/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: ditto_live - sha256: "51853b26df7b1de8495bb3bf6a674b1b15b3c60603e80397e0cdb9a66e44457e" + sha256: b8b7bc4b0c4fb211c750bf77bb877b16b31b5c82806f538dac88f2e36b8727c8 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.3" equatable: dependency: "direct main" description: @@ -469,5 +469,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.0" diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index 7244f6995..7aea07306 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -1,8 +1,8 @@ name: flutter_quickstart -description: "Demonstrates how to use the ditto_live plugin." +description: 'Demonstrates how to use the ditto_live plugin.' # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: "none" # Remove this line if you wish to publish to pub.dev +publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -19,8 +19,8 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=3.3.0 <4.0.0" - flutter: ">=3.24.0" + sdk: '>=3.3.0 <4.0.0' + flutter: '>=3.24.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -32,7 +32,7 @@ dependencies: flutter: sdk: flutter - ditto_live: 5.0.0 + ditto_live: 5.0.3 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -67,8 +67,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - # environment file for Ditto Config values - - .env + # environment file for Ditto Config values + - .env # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg diff --git a/flutter_app/test/dql_safety_test.dart b/flutter_app/test/dql_safety_test.dart new file mode 100644 index 000000000..ccecc74ff --- /dev/null +++ b/flutter_app/test/dql_safety_test.dart @@ -0,0 +1,71 @@ +// Regression test for the parameterized-DQL fix. +// +// Earlier versions of this file built UPDATE statements by interpolating +// user-typed task titles into DQL strings, e.g.: +// +// "UPDATE tasks SET title = '${newTask.title}' where _id = '${task.id}'" +// +// A task titled `'); EVICT FROM tasks WHERE true; --` could break out of +// the quoted value and run arbitrary DQL. The fix switched to named +// parameter binding (`:title`, `:done`, `:id`) with values passed via the +// `arguments:` map. +// +// This test pins the fix as a static-source check: any future change that +// reintroduces Dart string interpolation into the DQL strings in +// `lib/tasks_repository.dart` fails here loudly. It does not exercise the DQL +// engine — a real round-trip test would need a live `Ditto` instance, which +// lives in `integration_test/`. The check is intentionally coarse and +// operates on the file as text. + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('tasks_repository.dart DQL safety', () { + late String source; + + setUpAll(() { + source = File('lib/tasks_repository.dart').readAsStringSync(); + }); + + test('uses named parameter binding for every mutable field', () { + expect(source, contains(':task'), reason: 'INSERT binding missing'); + expect(source, contains(':title'), reason: 'updateTitle binding missing'); + expect(source, contains(':done'), reason: 'setDone binding missing'); + expect(source, contains(':id'), + reason: 'WHERE _id = :id binding missing'); + }); + + test('no Dart string interpolation inside DQL execute() calls', () { + // Find every `_ditto!.store.execute(` (or `.execute(`) call site and + // inspect the string literal that follows up to the next `,` or `)`. + // Forbid `${` and `$identifier` inside those literals. + final calls = RegExp(r'\.execute\(\s*"([^"]*)"').allMatches(source); + expect( + calls.isNotEmpty, + isTrue, + reason: 'No execute() calls found — has tasks_repository.dart moved?', + ); + + final shortInterp = RegExp(r'\$[a-zA-Z_{][a-zA-Z0-9_]*'); + for (final m in calls) { + final query = m.group(1)!; + expect( + query, + isNot(contains(r'${')), + reason: 'Found `\${` inside DQL: $query — use named binding.', + ); + final hit = shortInterp.firstMatch(query); + expect( + hit, + isNull, + reason: hit == null + ? '' + : 'Found `${hit.group(0)}` (Dart interpolation) inside DQL: ' + '$query — replace with `:name` and pass via `arguments:`.', + ); + } + }); + }); +} diff --git a/flutter_app/test/widget_test.dart b/flutter_app/test/widget_test.dart index 1b4d96200..9639126ce 100644 --- a/flutter_app/test/widget_test.dart +++ b/flutter_app/test/widget_test.dart @@ -39,11 +39,7 @@ void main() { }); test('toJson omits null id', () { - const task = Task( - title: 'New task', - done: false, - deleted: false, - ); + const task = Task(title: 'New task', done: false, deleted: false); final json = task.toJson(); @@ -69,8 +65,9 @@ void main() { }); group('Add task dialog', () { - testWidgets('shows Add Task title for new task', - (WidgetTester tester) async { + testWidgets('shows Add Task title for new task', ( + WidgetTester tester, + ) async { await tester.pumpWidget( MaterialApp( home: Builder( @@ -91,8 +88,9 @@ void main() { expect(find.text('Cancel'), findsOneWidget); }); - testWidgets('shows Edit Task title when editing existing task', - (WidgetTester tester) async { + testWidgets('shows Edit Task title when editing existing task', ( + WidgetTester tester, + ) async { const existing = Task( id: '1', title: 'Existing task', diff --git a/flutter_app/windows/CMakeLists.txt b/flutter_app/windows/CMakeLists.txt index 8943541fe..5971c91f9 100644 --- a/flutter_app/windows/CMakeLists.txt +++ b/flutter_app/windows/CMakeLists.txt @@ -53,6 +53,14 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) add_subdirectory("runner") +# permission_handler_windows (0.2.1, latest) requests C++20 but forces the +# legacy coroutine path via `/await`, which pulls in . +# The MSVC 14.51 (VS2026) STL turns that into hard error STL1011. Define the +# silence macro for the targets below (including the plugins added next) until +# the plugin migrates to the C++20 header. See SDKS-3997 and +# upstream Baseflow/flutter-permission-handler#1534. +add_compile_definitions(_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS) + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) diff --git a/global.json b/global.json new file mode 100644 index 000000000..085a86675 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "//": "The quickstart .NET apps target net10.0; this shadows the monorepo root global.json (which pins the 8.x SDK the core repo builds with) so `dotnet` resolves a .NET 10 SDK for everything under quickstart/.", + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/go-tui/README.md b/go-tui/README.md index 17e26f69f..2d8a6cdf7 100644 --- a/go-tui/README.md +++ b/go-tui/README.md @@ -7,8 +7,8 @@ a todo list that syncs between multiple peers. ## Getting Started To get started, you'll first need to create an app in the [Ditto Portal][0] -with the "Online Playground" authentication type. You'll need to find your -AppID and Online Playground Token, Auth URL, and Websocket URL in order to use this quickstart. +with the "Development" authentication type. You'll need to find your +Database ID, Development Token, and Server URL in order to use this quickstart. [0]: https://portal.ditto.live @@ -17,18 +17,18 @@ Create a `.env` file in this directory with your Ditto credentials: ```bash # Create .env file cat > .env << 'EOF' -DITTO_APP_ID=your-app-id -DITTO_PLAYGROUND_TOKEN=your-playground-token -DITTO_AUTH_URL=https://your-app-id.cloud.ditto.live +DITTO_DATABASE_ID=your-database-id +DITTO_DEVELOPMENT_TOKEN=your-development-token +DITTO_SERVER_URL=https://your-server-url EOF ``` Alternatively, you can set these as environment variables: ```bash -export DITTO_APP_ID="your-app-id" -export DITTO_PLAYGROUND_TOKEN="your-playground-token" -export DITTO_AUTH_URL="https://your-app-id.cloud.ditto.live" +export DITTO_DATABASE_ID="your-database-id" +export DITTO_DEVELOPMENT_TOKEN="your-development-token" +export DITTO_SERVER_URL="https://your-server-url" ``` ## Building diff --git a/go-tui/ditto_manager.go b/go-tui/ditto_manager.go new file mode 100644 index 000000000..9f054056c --- /dev/null +++ b/go-tui/ditto_manager.go @@ -0,0 +1,85 @@ +package main + +import ( + "log" + "os" + "time" + + "github.com/getditto/ditto-go-sdk/v5/ditto" +) + +// DittoManager owns everything about configuring and running the Ditto +// instance: opening it, wiring up development authentication, and starting +// sync. It knows nothing about tasks. +// +// It vends a configured, already-running Ditto instance (sync is started in +// NewDittoManager) that the rest of the app uses directly via Ditto(). +type DittoManager struct { + ditto *ditto.Ditto + tempDir string +} + +// NewDittoManager opens a Ditto instance using the Server connection API, +// installs the development-mode authentication handler, and starts sync. +func NewDittoManager(databaseId, token, serverURL string) (*DittoManager, error) { + // Create a temp directory for persistence. Using a temporary directory + // means data is not persistent between runs, but it lets multiple + // instances run concurrently on the same machine. + tempDir, err := os.MkdirTemp("", "ditto-quickstart-*") + if err != nil { + return nil, err + } + + // Initialize Ditto with the Server connection API + config := ditto.DefaultDittoConfig(). + WithDatabaseID(databaseId). + WithPersistenceDirectory(tempDir). + WithConnect(&ditto.DittoConfigConnectServer{URL: serverURL}) + + d, err := ditto.Open(config) + if err != nil { + os.RemoveAll(tempDir) + return nil, err + } + + // Set up authentication handler for development mode + if auth := d.Auth(); auth != nil { + auth.SetExpirationHandler( + func(d *ditto.Ditto, timeUntilExpiration time.Duration) { + log.Printf("Expiration handler called with time until expiration: %v", timeUntilExpiration) + + // For development mode, login with the development token + provider := ditto.DevelopmentAuthenticationProvider() + clientInfoJSON, err := d.Auth().Login(token, provider) + if err != nil { + log.Printf("Failed to login: %v", err) + } else { + log.Printf("Login successful") + if clientInfoJSON != "" { + log.Printf("Client info: %s", clientInfoJSON) + } + } + }) + } + + // Start sync (authentication handler will be called automatically if needed) + if err := d.Sync().Start(); err != nil { + d.Close() + os.RemoveAll(tempDir) + return nil, err + } + + return &DittoManager{ditto: d, tempDir: tempDir}, nil +} + +// Ditto exposes the running Ditto instance so callers can use the real Ditto +// API directly. +func (m *DittoManager) Ditto() *ditto.Ditto { + return m.ditto +} + +// Close shuts down the Ditto instance and removes the persistence directory. +func (m *DittoManager) Close() { + m.ditto.Close() + os.RemoveAll(m.tempDir) +} diff --git a/go-tui/main.go b/go-tui/main.go index 673703fae..241efc83a 100644 --- a/go-tui/main.go +++ b/go-tui/main.go @@ -12,17 +12,9 @@ import ( "github.com/getditto/ditto-go-sdk/v5/ditto" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" - "github.com/google/uuid" "github.com/joho/godotenv" ) -type Task struct { - ID string `json:"_id"` - Title string `json:"title"` - Done bool `json:"done"` - Deleted bool `json:"deleted"` -} - type InputMode int const ( @@ -32,12 +24,10 @@ const ( ) type App struct { - ditto *ditto.Ditto - observer *ditto.StoreObserver - subscription *ditto.SyncSubscription + repo *TasksRepository tasks []Task - tasksChan chan []Task + tasksChan <-chan []Task selectedIdx int inputMode InputMode @@ -83,57 +73,21 @@ func main() { } // Get config from environment - appID := os.Getenv("DITTO_APP_ID") - token := os.Getenv("DITTO_PLAYGROUND_TOKEN") - authURL := os.Getenv("DITTO_AUTH_URL") - - if appID == "" || token == "" || authURL == "" { - log.Fatal("Missing required environment variables. Please set DITTO_APP_ID, DITTO_PLAYGROUND_TOKEN, and DITTO_AUTH_URL") - } + databaseId := os.Getenv("DITTO_DATABASE_ID") + token := os.Getenv("DITTO_DEVELOPMENT_TOKEN") + serverURL := os.Getenv("DITTO_SERVER_URL") - // Create temp directory for persistence - tempDir, err := os.MkdirTemp("", "ditto-quickstart-*") - if err != nil { - log.Fatal(err) + if databaseId == "" || token == "" || serverURL == "" { + log.Fatal("Missing required environment variables. Please set DITTO_DATABASE_ID, DITTO_DEVELOPMENT_TOKEN, and DITTO_SERVER_URL") } - defer os.RemoveAll(tempDir) - - // Initialize Ditto with Server connection API - config := ditto.DefaultDittoConfig(). - WithDatabaseID(appID). - WithPersistenceDirectory(tempDir). - WithConnect(&ditto.DittoConfigConnectServer{URL: authURL}) - d, err := ditto.Open(config) + // Open and start the Ditto instance. The manager owns everything about + // configuring and running Ditto; it knows nothing about tasks. + manager, err := NewDittoManager(databaseId, token, serverURL) if err != nil { log.Fatal("Failed to open Ditto:", err) } - defer d.Close() - - // Set up authentication handler for development mode - if auth := d.Auth(); auth != nil { - auth.SetExpirationHandler( - func(d *ditto.Ditto, timeUntilExpiration time.Duration) { - log.Printf("Expiration handler called with time until expiration: %v", timeUntilExpiration) - - // For development mode, login with the playground token - provider := ditto.DevelopmentAuthenticationProvider() - clientInfoJSON, err := d.Auth().Login(token, provider) - if err != nil { - log.Printf("Failed to login: %v", err) - } else { - log.Printf("Login successful") - if clientInfoJSON != "" { - log.Printf("Client info: %s", clientInfoJSON) - } - } - }) - } - - // Start sync (authentication handler will be called automatically if needed) - if err := d.Sync().Start(); err != nil { - log.Fatal("Failed to start sync:", err) - } + defer manager.Close() // Initialize termui if err := ui.Init(); err != nil { @@ -141,48 +95,37 @@ func main() { } defer ui.Close() + // The repository owns everything specific to the tasks data: the sync + // subscription, the store observer, and task CRUD. + repo := NewTasksRepository(manager) + // Create app - app := NewApp(d) + app := NewApp(repo) - // Create subscription for syncing - subscription, err := d.Sync().RegisterSubscription("SELECT * FROM tasks") - if err != nil { + // Subscribe so Ditto syncs the tasks collection from other peers + if err := repo.RegisterSubscription(); err != nil { log.Fatal("Failed to register subscription:", err) } - defer subscription.Cancel() - app.subscription = subscription - - // Create observer for local changes - observer, err := d.Store().RegisterObserver( - "SELECT * FROM tasks WHERE deleted = false ORDER BY _id", - nil, - func(result *ditto.QueryResult) { - defer result.Close() - - tasks := parseTasks(result) - select { - case app.tasksChan <- tasks: - case <-app.ctx.Done(): - } - }) + + // Observe local changes and feed the task list into the app + tasksChan, err := repo.ObserveTasks(app.ctx) if err != nil { log.Fatal("Failed to register observer:", err) } - defer observer.Cancel() - app.observer = observer + defer repo.Close() + app.tasksChan = tasksChan // Run the app app.Run() } -func NewApp(d *ditto.Ditto) *App { +func NewApp(repo *TasksRepository) *App { ctx, cancel := context.WithCancel(context.Background()) app := &App{ - ditto: d, + repo: repo, tasks: []Task{}, selectedIdx: 0, inputMode: NormalMode, - tasksChan: make(chan []Task, 1), // Buffer size 1 - latest update wins ctx: ctx, cancel: cancel, } @@ -427,64 +370,27 @@ func (a *App) updateTable() { } func (a *App) createTask(title string) { - task := map[string]interface{}{ - "_id": uuid.New().String(), - "title": title, - "done": false, - "deleted": false, - } - - result, err := a.ditto.Store().Execute( - "INSERT INTO tasks VALUES (:task)", - ditto.QueryArguments{"task": task}, - ) - if err != nil { + if err := a.repo.CreateTask(title); err != nil { a.setError(err.Error()) - return } - defer result.Close() } func (a *App) updateTask(id, title string) { - result, err := a.ditto.Store().Execute( - "UPDATE tasks SET title = :title WHERE _id = :id", - ditto.QueryArguments{ - "title": title, - "id": id, - }, - ) - if err != nil { + if err := a.repo.UpdateTask(id, title); err != nil { a.setError(err.Error()) - return } - defer result.Close() } func (a *App) toggleTask(id string, done bool) { - result, err := a.ditto.Store().Execute( - "UPDATE tasks SET done = :done WHERE _id = :id", - ditto.QueryArguments{ - "done": done, - "id": id, - }, - ) - if err != nil { + if err := a.repo.ToggleTask(id, done); err != nil { a.setError(err.Error()) - return } - defer result.Close() } func (a *App) deleteTask(id string) { - result, err := a.ditto.Store().Execute( - "UPDATE tasks SET deleted = true WHERE _id = :id", - ditto.QueryArguments{"id": id}, - ) - if err != nil { + if err := a.repo.DeleteTask(id); err != nil { a.setError(err.Error()) - return } - defer result.Close() } // Set the UI error message. May be called by any goroutine. @@ -509,22 +415,6 @@ func loadEnv() error { return fmt.Errorf(".env file not found") } -func parseTasks(result *ditto.QueryResult) []Task { - if result == nil { - return []Task{} - } - - tasks := make([]Task, 0, result.ItemCount()) - for _, queryItem := range result.Items() { - var task Task - if err := queryItem.UnmarshalTo(&task); err != nil { - panic(err) - } - tasks = append(tasks, task) - } - return tasks -} - // getSelectedTask returns the currently selected task and whether the selection is valid func (a *App) getSelectedTask() (Task, bool) { if a.selectedIdx < len(a.tasks) { diff --git a/go-tui/tasks_repository.go b/go-tui/tasks_repository.go new file mode 100644 index 000000000..3307c0029 --- /dev/null +++ b/go-tui/tasks_repository.go @@ -0,0 +1,154 @@ +package main + +import ( + "context" + + "github.com/getditto/ditto-go-sdk/v5/ditto" + "github.com/google/uuid" +) + +// Task is a single todo item as stored in the "tasks" collection. +type Task struct { + ID string `json:"_id"` + Title string `json:"title"` + Done bool `json:"done"` + Deleted bool `json:"deleted"` +} + +// TasksRepository owns everything specific to the tasks data: the sync +// subscription, the store observer that streams the current task list, and the +// task CRUD operations. It talks to Ditto directly through the instance vended +// by DittoManager. +type TasksRepository struct { + ditto *ditto.Ditto + subscription *ditto.SyncSubscription + observer *ditto.StoreObserver +} + +// NewTasksRepository creates a repository backed by the manager's Ditto +// instance. +func NewTasksRepository(manager *DittoManager) *TasksRepository { + return &TasksRepository{ditto: manager.Ditto()} +} + +// RegisterSubscription subscribes this peer to the tasks collection so Ditto +// syncs matching documents from other peers. +func (r *TasksRepository) RegisterSubscription() error { + subscription, err := r.ditto.Sync().RegisterSubscription("SELECT * FROM tasks") + if err != nil { + return err + } + r.subscription = subscription + return nil +} + +// ObserveTasks registers a store observer against the local database and +// returns a channel that receives the current task list whenever it changes. +func (r *TasksRepository) ObserveTasks(ctx context.Context) (<-chan []Task, error) { + tasksChan := make(chan []Task, 1) // Buffer size 1 - latest update wins + + observer, err := r.ditto.Store().RegisterObserver( + "SELECT * FROM tasks WHERE NOT deleted", + nil, + func(result *ditto.QueryResult) { + defer result.Close() + + tasks := parseTasks(result) + select { + case tasksChan <- tasks: + case <-ctx.Done(): + } + }) + if err != nil { + return nil, err + } + r.observer = observer + return tasksChan, nil +} + +func (r *TasksRepository) CreateTask(title string) error { + task := map[string]interface{}{ + "_id": uuid.New().String(), + "title": title, + "done": false, + "deleted": false, + } + + result, err := r.ditto.Store().Execute( + "INSERT INTO tasks DOCUMENTS (:task)", + ditto.QueryArguments{"task": task}, + ) + if err != nil { + return err + } + result.Close() + return nil +} + +func (r *TasksRepository) UpdateTask(id, title string) error { + result, err := r.ditto.Store().Execute( + "UPDATE tasks SET title = :title WHERE _id = :id", + ditto.QueryArguments{ + "title": title, + "id": id, + }, + ) + if err != nil { + return err + } + result.Close() + return nil +} + +func (r *TasksRepository) ToggleTask(id string, done bool) error { + result, err := r.ditto.Store().Execute( + "UPDATE tasks SET done = :done WHERE _id = :id", + ditto.QueryArguments{ + "done": done, + "id": id, + }, + ) + if err != nil { + return err + } + result.Close() + return nil +} + +func (r *TasksRepository) DeleteTask(id string) error { + result, err := r.ditto.Store().Execute( + "UPDATE tasks SET deleted = true WHERE _id = :id", + ditto.QueryArguments{"id": id}, + ) + if err != nil { + return err + } + result.Close() + return nil +} + +// Close cancels the observer and subscription held by the repository. +func (r *TasksRepository) Close() { + if r.observer != nil { + r.observer.Cancel() + } + if r.subscription != nil { + r.subscription.Cancel() + } +} + +func parseTasks(result *ditto.QueryResult) []Task { + if result == nil { + return []Task{} + } + + tasks := make([]Task, 0, result.ItemCount()) + for _, queryItem := range result.Items() { + var task Task + if err := queryItem.UnmarshalTo(&task); err != nil { + panic(err) + } + tasks = append(tasks, task) + } + return tasks +} diff --git a/java-server/README.md b/java-server/README.md index 89a4d2071..a5cf8ee62 100644 --- a/java-server/README.md +++ b/java-server/README.md @@ -6,8 +6,8 @@ For more information, see - [Java Install Guide](https://docs.ditto.live/sdk/lat ## Getting Started -1. Create an application at . Make note of the app ID and online playground token. -2. Copy the `.env.template` file at the top level of the `quickstart` repo to `.env` and add your app ID and online playground token. +1. Create an application at . Make note of the Database ID and development token. +2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID and development token. 3. Synchronize your project with the Gradle file by clicking Build > Sync Project with Gradle Files. 4. Open a terminal and run the following command to launch the app: - `./gradlew bootRun` @@ -16,5 +16,5 @@ For more information, see - [Java Install Guide](https://docs.ditto.live/sdk/lat ## Additional Resources - [Java Roadmap and Support Policy](https://docs.ditto.live/sdk/latest/install-guides/java/roadmap) -- [API Reference](https://software.ditto.live/java/ditto-java/5.0.0/api-reference/) +- [API Reference](https://docs.ditto.live/sdk/latest/api-reference/java) diff --git a/java-server/build.gradle.kts b/java-server/build.gradle.kts index d3099051c..3a2b620db 100644 --- a/java-server/build.gradle.kts +++ b/java-server/build.gradle.kts @@ -31,29 +31,29 @@ spotbugs { dependencies { // ditto-java artifact includes the Java API for Ditto - implementation("com.ditto:ditto-java:5.0.0") + implementation("com.ditto:ditto-java:5.0.3") // This will include binaries for all the supported platforms and architectures - implementation("com.ditto:ditto-binaries:5.0.0") + implementation("com.ditto:ditto-binaries:5.0.3") // To reduce your module artifact's size, consider including just the necessary platforms and architectures /* // macOS Apple Silicon - implementation("com.ditto:ditto-binaries:5.0.0") { + implementation("com.ditto:ditto-binaries:5.0.3") { capabilities { requireCapability("com.ditto:ditto-binaries-macos-arm64") } } // Windows x86_64 - implementation("com.ditto:ditto-binaries:5.0.0") { + implementation("com.ditto:ditto-binaries:5.0.3") { capabilities { requireCapability("com.ditto:ditto-binaries-windows-x64") } } // Linux x86_64 - implementation("com.ditto:ditto-binaries:5.0.0") { + implementation("com.ditto:ditto-binaries:5.0.3") { capabilities { requireCapability("com.ditto:ditto-binaries-linux-x64") } diff --git a/java-server/java-spring-maven/pom.xml b/java-server/java-spring-maven/pom.xml index a8ed7ec43..f6af91fd4 100644 --- a/java-server/java-spring-maven/pom.xml +++ b/java-server/java-spring-maven/pom.xml @@ -34,14 +34,14 @@ com.ditto ditto-java - 5.0.0-preview.3 + 5.0.3 com.ditto ditto-binaries - 5.0.0-preview.3 + 5.0.3 diff --git a/java-server/settings.gradle.kts b/java-server/settings.gradle.kts index 46a5b6491..2dd67738e 100644 --- a/java-server/settings.gradle.kts +++ b/java-server/settings.gradle.kts @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } rootProject.name = "quickstart-java" diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/DittoConfigRestController.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/DittoConfigRestController.java index 6b8176cd3..351872b47 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/DittoConfigRestController.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/DittoConfigRestController.java @@ -1,6 +1,6 @@ package com.ditto.example.spring.quickstart.controller; -import com.ditto.example.spring.quickstart.service.DittoService; +import com.ditto.example.spring.quickstart.service.DittoManager; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerSentEvent; import org.springframework.web.bind.annotation.GetMapping; @@ -12,25 +12,48 @@ @RestController public class DittoConfigRestController { - private final DittoService dittoService; + private final DittoManager dittoManager; - public DittoConfigRestController(final DittoService dittoService) { - this.dittoService = dittoService; + public DittoConfigRestController(final DittoManager dittoManager) { + this.dittoManager = dittoManager; } - @PostMapping("/ditto/sync/toggle") - public String toggleSync() { - dittoService.toggleSync(); + // Idempotent desired-state endpoints — there is no blind "toggle". A repeated + // request (retry, stale double-click) cannot reverse the intended result. + @PostMapping("/ditto/sync/start") + public String startSync() { + dittoManager.setSyncEnabled(true); return ""; } + @PostMapping("/ditto/sync/stop") + public String stopSync() { + dittoManager.setSyncEnabled(false); + return ""; + } + + // Streams the sync control (current state label + the action button that + // reflects it) so the displayed state and the control always update together. @GetMapping(path = "/ditto/sync/state", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @ResponseBody public Flux> syncState() { - return dittoService.getSyncState() - .map(syncState -> ServerSentEvent.builder("Sync State: %s".formatted(syncState)) + return dittoManager.getSyncState() + .map(enabled -> ServerSentEvent.builder(renderSyncControl(enabled)) .event("sync_state") .build() ); } + + // Server-rendered control fragment. When sync is enabled the button offers to + // stop it; when disabled it offers to start it — the label always states the + // action, never an ambiguous "Toggle". Single line: SSE data must not contain + // raw newlines. + private static String renderSyncControl(boolean enabled) { + if (enabled) { + return "Sync State: Enabled" + + ""; + } + return "Sync State: Disabled" + + ""; + } } diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskContentController.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskContentController.java index 7de48c0b7..c304a0913 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskContentController.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskContentController.java @@ -1,6 +1,6 @@ package com.ditto.example.spring.quickstart.controller; -import com.ditto.example.spring.quickstart.service.DittoTaskService; +import com.ditto.example.spring.quickstart.service.TasksRepository; import com.ditto.example.spring.quickstart.service.Task; import jakarta.annotation.Nonnull; import org.jetbrains.annotations.NotNull; @@ -16,15 +16,15 @@ @Controller public class TaskContentController { @Nonnull - private final DittoTaskService taskService; + private final TasksRepository tasksRepository; - public TaskContentController(@NotNull final DittoTaskService taskService) { - this.taskService = taskService; + public TaskContentController(@NotNull final TasksRepository tasksRepository) { + this.tasksRepository = tasksRepository; } @GetMapping("/") public String index(Map model) { - List tasks = taskService.observeAll().blockFirst(); + List tasks = tasksRepository.observeAll().blockFirst(); model.put("tasks", tasks != null ? tasks : Collections.emptyList()); return "index"; } diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java index 9002c792a..97d560a87 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java @@ -1,11 +1,17 @@ package com.ditto.example.spring.quickstart.controller; -import com.ditto.example.spring.quickstart.service.DittoTaskService; +import com.ditto.example.spring.quickstart.service.TasksRepository; import com.ditto.example.spring.quickstart.service.Task; import jakarta.annotation.Nonnull; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerSentEvent; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.thymeleaf.context.Context; import org.thymeleaf.spring6.SpringTemplateEngine; import reactor.core.publisher.Flux; @@ -16,18 +22,18 @@ @RestController public class TaskRestController { @Nonnull - private final DittoTaskService taskService; + private final TasksRepository tasksRepository; @Nonnull private final SpringTemplateEngine templateEngine; - public TaskRestController(final DittoTaskService taskService, final SpringTemplateEngine templateEngine) { - this.taskService = taskService; + public TaskRestController(final TasksRepository tasksRepository, final SpringTemplateEngine templateEngine) { + this.tasksRepository = tasksRepository; this.templateEngine = templateEngine; } @GetMapping(value = "/tasks/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux> streamTasks() { - return taskService.observeAll().map(tasks -> { + return tasksRepository.observeAll().map(tasks -> { String htmlFragment = renderTodoList(tasks); return ServerSentEvent.builder(htmlFragment) .event("task_list") @@ -37,25 +43,25 @@ public Flux> streamTasks() { @PostMapping("/tasks") public String addTask(@RequestParam("title") @Nonnull String title) { - taskService.addTask(title); + tasksRepository.addTask(title); return ""; } @PostMapping("/tasks/{taskId}/toggle") - public String toggleTaskDone(@PathVariable @Nonnull String taskId) { - taskService.toggleTaskDone(taskId); + public String toggleTaskDone(@PathVariable @Nonnull String taskId, @RequestParam boolean done) { + tasksRepository.setTaskDone(taskId, done); return ""; } @DeleteMapping("/tasks/{taskId}") public String deleteTask(@PathVariable @Nonnull String taskId) { - taskService.deleteTask(taskId); + tasksRepository.deleteTask(taskId); return ""; } @PutMapping("/tasks/{taskId}") public String updateTask(@PathVariable @Nonnull String taskId, @RequestParam @Nonnull String title) { - taskService.updateTask(taskId, title); + tasksRepository.updateTask(taskId, title); return ""; } diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoManager.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoManager.java new file mode 100644 index 000000000..fc0e08e65 --- /dev/null +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoManager.java @@ -0,0 +1,130 @@ +package com.ditto.example.spring.quickstart.service; + +import com.ditto.example.spring.quickstart.configuration.DittoConfigurationKeys; +import com.ditto.example.spring.quickstart.configuration.DittoSecretsConfiguration; +import com.ditto.java.Ditto; +import com.ditto.java.DittoAsyncCancellable; +import com.ditto.java.DittoAuthenticationProvider; +import com.ditto.java.DittoConfig; +import com.ditto.java.DittoConnection; +import com.ditto.java.DittoException; +import com.ditto.java.DittoFactory; +import com.ditto.java.DittoPeer; +import jakarta.annotation.Nonnull; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; + +import java.io.File; + +@Component +public class DittoManager implements DisposableBean { + @Nonnull + private final Ditto ditto; + + @Nonnull + private final DittoAsyncCancellable presenceObserver; + + // Initialized to false and only flipped to true after sync().start() succeeds + // (see setSyncEnabled), so the streamed state reflects sync actually starting + // rather than a matching default — that keeps the startup regression test honest. + @Nonnull + private final Sinks.Many mutableSyncStatePublisher = Sinks.many().replay().latestOrDefault(false); + + // Whether Ditto sync is currently running. Starts enabled on init (see constructor); + // changed via the REST start/stop endpoints. Guarded by setSyncEnabled being + // synchronized (the only reader/writer), so it does not need to be volatile. + private boolean syncEnabled = false; + + private final Logger logger = LoggerFactory.getLogger(DittoManager.class); + + DittoManager(@Nonnull final Environment environment) { + File dittoDir = new File(environment.getRequiredProperty(DittoConfigurationKeys.DITTO_DIR)); + dittoDir.mkdirs(); + + /* + * Setup Ditto Config + * https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing + */ + DittoConfig dittoConfig = new DittoConfig.Builder(DittoSecretsConfiguration.DITTO_DATABASE_ID) +// .persistenceDirectory("/tmp/ditto-quickstart") + .serverConnect(DittoSecretsConfiguration.DITTO_SERVER_URL) + .build(); + + this.ditto = DittoFactory.create(dittoConfig); + + this.ditto.getAuth().setExpirationHandler((expiringDitto, _timeUntilExpiration) -> + expiringDitto.getAuth() + .login( + DittoSecretsConfiguration.DITTO_DEVELOPMENT_TOKEN, + DittoAuthenticationProvider.development() + ).thenRun(() -> { }) + ); + + this.ditto.setDeviceName("Java"); + + presenceObserver = observePeersPresence(); + + // Start sync on initialization so the quickstart syncs out of the box. + // The UI / REST start/stop endpoints can subsequently stop and restart it. + setSyncEnabled(true); + } + + @Override + public void destroy() throws Exception { + logger.info("Ditto is being closed"); + + presenceObserver.cancel(); + ditto.close(); + } + + @NotNull + public Ditto getDitto() { + return ditto; + } + + public Flux getSyncState() { + return mutableSyncStatePublisher.asFlux(); + } + + // Set the desired sync state. Idempotent: calling it with the already-active + // state is a no-op, so a repeated request / retry / stale double-click cannot + // flip the transport lifecycle. There is deliberately no blind "toggle". + // synchronized so the check-then-act (read syncEnabled, start/stop, write) is + // atomic across the concurrent REST start/stop endpoints. + // https://docs.ditto.live/sdk/latest/sync/start-and-stop-sync + public synchronized void setSyncEnabled(boolean enabled) { + if (enabled == syncEnabled) { + return; + } + if (enabled) { + try { + ditto.getSync().start(); + } catch (DittoException e) { + throw new RuntimeException(e); + } + } else { + ditto.getSync().stop(); + } + syncEnabled = enabled; + mutableSyncStatePublisher.tryEmitNext(enabled); + } + + private DittoAsyncCancellable observePeersPresence() { + return ditto.getPresence().observe((graph) -> { + logger.info("Peers connected: {}", graph.getRemotePeers().size()); + for (DittoPeer peer : graph.getRemotePeers()) { + logger.info("Peer: {}", peer.getDeviceName()); + for (DittoConnection connection : peer.getConnections()) { + logger.info("\t- {} {}", connection.getId(), connection.getConnectionType()); + } + } + }); + } + +} diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java deleted file mode 100644 index e65182c5e..000000000 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.ditto.example.spring.quickstart.service; - -import com.ditto.example.spring.quickstart.configuration.DittoConfigurationKeys; -import com.ditto.example.spring.quickstart.configuration.DittoSecretsConfiguration; -import com.ditto.java.*; -import com.ditto.java.serialization.DittoCborSerializable; -import jakarta.annotation.Nonnull; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Sinks; - -import java.io.File; -import java.util.List; - -@Component -public class DittoService implements DisposableBean { - private static final String DITTO_SYNC_STATE_COLLECTION = "spring_sync_state"; - private static final String DITTO_SYNC_STATE_ID = "sync_state"; - - @Nonnull - private final Ditto ditto; - - @Nonnull - private final DittoAsyncCancellable presenceObserver; - - @Nonnull - private final DittoStoreObserver syncStateObserver; - - @Nonnull - private final Sinks.Many mutableSyncStatePublisher = Sinks.many().replay().latestOrDefault(false); - - private final Logger logger = LoggerFactory.getLogger(DittoService.class); - - DittoService(@Nonnull final Environment environment) { - File dittoDir = new File(environment.getRequiredProperty(DittoConfigurationKeys.DITTO_DIR)); - dittoDir.mkdirs(); - - /* - * Setup Ditto Config - * https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing - */ - DittoConfig dittoConfig = new DittoConfig.Builder(DittoSecretsConfiguration.DITTO_APP_ID) -// .persistenceDirectory("/tmp/ditto-quickstart") - .serverConnect(DittoSecretsConfiguration.DITTO_AUTH_URL) - .build(); - - this.ditto = DittoFactory.create(dittoConfig); - - this.ditto.getAuth().setExpirationHandler((expiringDitto, _timeUntilExpiration) -> - expiringDitto.getAuth() - .login( - DittoSecretsConfiguration.DITTO_PLAYGROUND_TOKEN, - DittoAuthenticationProvider.development() - ).thenRun(() -> { }) - ); - - this.ditto.setDeviceName("Java"); - - presenceObserver = observePeersPresence(); - - syncStateObserver = setupAndObserveSyncState(); - } - - @Override - public void destroy() throws Exception { - logger.info("Ditto is being closed"); - - presenceObserver.cancel(); - syncStateObserver.close(); - ditto.close(); - } - - @NotNull - public Ditto getDitto() { - return ditto; - } - - public Flux getSyncState() { - return mutableSyncStatePublisher.asFlux(); - } - - public void toggleSync() { - boolean currentSyncState = Boolean.TRUE.equals(mutableSyncStatePublisher.asFlux().blockFirst()); - setSyncStateIntoDittoStore(!currentSyncState); - } - - private DittoAsyncCancellable observePeersPresence() { - return ditto.getPresence().observe((graph) -> { - logger.info("Peers connected: {}", graph.getRemotePeers().size()); - for (DittoPeer peer : graph.getRemotePeers()) { - logger.info("Peer: {}", peer.getDeviceName()); - for (DittoConnection connection : peer.getConnections()) { - logger.info("\t- {} {}", connection.getId(), connection.getConnectionType()); - } - } - }); - } - - private DittoStoreObserver setupAndObserveSyncState() { - try { - boolean hasNoSyncState = ditto.getStore().executeRaw( - "SELECT * FROM %s".formatted(DITTO_SYNC_STATE_COLLECTION) - ).toCompletableFuture().join().getItems().isEmpty(); - if (hasNoSyncState) { - ditto.getStore().execute( - "INSERT INTO %s DOCUMENTS(:sync)".formatted(DITTO_SYNC_STATE_COLLECTION), - DittoCborSerializable.buildDictionary() - .put("sync", DittoCborSerializable.buildDictionary() - .put("_id", DITTO_SYNC_STATE_ID) - .put(DITTO_SYNC_STATE_ID, false) - .build() - ) - .build() - ).toCompletableFuture().join(); - } - - return ditto.getStore().registerObserver( - "SELECT * FROM %s WHERE _id = :id".formatted(DITTO_SYNC_STATE_COLLECTION), - DittoCborSerializable.buildDictionary() - .put("id", DITTO_SYNC_STATE_ID) - .build(), - (result) -> { - List items = result.getItems(); - boolean newSyncState = false; - try { - if (!items.isEmpty()) { - newSyncState = items.get(0).getValue() - .get(DITTO_SYNC_STATE_ID) - .asBoolean(); - } - } catch (DittoException e) { - logger.error("Error: {}", String.valueOf(e)); - } - - if (newSyncState) { - try { - ditto.getSync().start(); - } catch (DittoException e) { - throw new RuntimeException(e); - } - } else { - ditto.getSync().stop(); - } - - mutableSyncStatePublisher.tryEmitNext(newSyncState); - }); - } catch (DittoException e) { - throw new RuntimeException(e); - } - } - - private void setSyncStateIntoDittoStore(boolean newState) { - ditto.getStore().execute( - "UPDATE %s SET %s = :syncState".formatted(DITTO_SYNC_STATE_COLLECTION, DITTO_SYNC_STATE_ID), - DittoCborSerializable.buildDictionary() - .put("syncState", newState) - .build() - ).toCompletableFuture().join(); - } -} diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/TasksRepository.java similarity index 65% rename from java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java rename to java-server/src/main/java/com/ditto/example/spring/quickstart/service/TasksRepository.java index 0da0e127d..16d4a7c2e 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/TasksRepository.java @@ -1,6 +1,10 @@ package com.ditto.example.spring.quickstart.service; -import com.ditto.java.*; +import com.ditto.java.Ditto; +import com.ditto.java.DittoException; +import com.ditto.java.DittoQueryResultItem; +import com.ditto.java.DittoStoreObserver; +import com.ditto.java.DittoSyncSubscription; import com.ditto.java.serialization.DittoCborSerializable; import jakarta.annotation.Nonnull; @@ -13,22 +17,22 @@ import java.util.UUID; @Component -public class DittoTaskService { +public class TasksRepository { private static final String TASKS_COLLECTION_NAME = "tasks"; - private final DittoService dittoService; + private final DittoManager dittoManager; - public DittoTaskService(DittoService dittoService) { - this.dittoService = dittoService; + public TasksRepository(DittoManager dittoManager) { + this.dittoManager = dittoManager; } public void addTask(@Nonnull String title) { try { - dittoService.getDitto().getStore().execute( - "INSERT INTO %s DOCUMENTS (:newTask)".formatted(TASKS_COLLECTION_NAME), + dittoManager.getDitto().getStore().execute( + "INSERT INTO %s DOCUMENTS (:task)".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() .put( - "newTask", + "task", DittoCborSerializable.Dictionary.buildDictionary() .put("_id", UUID.randomUUID().toString()) .put("title", title) @@ -43,36 +47,26 @@ public void addTask(@Nonnull String title) { } } - public void toggleTaskDone(@Nonnull String taskId) { + public void setTaskDone(@Nonnull String taskId, boolean done) { try { - DittoQueryResult tasks = dittoService.getDitto().getStore().executeRaw( - "SELECT * FROM %s WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + dittoManager.getDitto().getStore().execute( + "UPDATE %s SET done = :done WHERE _id = :id".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("taskId", taskId) + .put("done", done) + .put("id", taskId) .build() ).toCompletableFuture().join(); - - boolean isDone = tasks.getItems().get(0).getValue().get("done").asBoolean(); - - dittoService.getDitto().getStore().execute( - "UPDATE %s SET done = :done WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), - DittoCborSerializable.Dictionary.buildDictionary() - .put("done", !isDone) - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); - } catch (Error | DittoException e) { + } catch (Error e) { throw new RuntimeException(e); } } public void deleteTask(@Nonnull String taskId) { try { - dittoService.getDitto().getStore().execute( - "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + dittoManager.getDitto().getStore().execute( + "UPDATE %s SET deleted = true WHERE _id = :id".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("deleted", true) - .put("taskId", taskId) + .put("id", taskId) .build() ).toCompletableFuture().join(); } catch (Error e) { @@ -82,11 +76,11 @@ public void deleteTask(@Nonnull String taskId) { public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { try { - dittoService.getDitto().getStore().execute( - "UPDATE %s SET title = :title WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + dittoManager.getDitto().getStore().execute( + "UPDATE %s SET title = :title WHERE _id = :id".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() .put("title", newTitle) - .put("taskId", taskId) + .put("id", taskId) .build() ).toCompletableFuture().join(); } catch (Error e) { @@ -96,11 +90,15 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { @Nonnull public Flux> observeAll() { - final String subscriptionQuery = "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); - final String displayQuery = subscriptionQuery + " ORDER BY title ASC"; + // The subscription controls what syncs to this device and is written to + // the local database; the observer reacts to changes in that local + // database, hiding soft-deleted tasks from the returned list. + final String subscriptionQuery = "SELECT * FROM %s".formatted(TASKS_COLLECTION_NAME); + final String displayQuery = + "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); return Flux.create(emitter -> { - Ditto ditto = dittoService.getDitto(); + Ditto ditto = dittoManager.getDitto(); try { DittoSyncSubscription subscription = ditto.getSync().registerSubscription(subscriptionQuery); DittoStoreObserver observer = ditto.getStore().registerObserver(displayQuery, results -> diff --git a/java-server/src/main/resources/templates/fragments/taskList.html b/java-server/src/main/resources/templates/fragments/taskList.html index bf650424f..c9b99374b 100644 --- a/java-server/src/main/resources/templates/fragments/taskList.html +++ b/java-server/src/main/resources/templates/fragments/taskList.html @@ -1,7 +1,7 @@
  • + +
    + Sync State: … +
    diff --git a/java-server/src/test/java/com/ditto/example/spring/quickstart/TaskVisibilityIntegrationTest.java b/java-server/src/test/java/com/ditto/example/spring/quickstart/TaskVisibilityIntegrationTest.java index 4245228de..1d1d26b05 100644 --- a/java-server/src/test/java/com/ditto/example/spring/quickstart/TaskVisibilityIntegrationTest.java +++ b/java-server/src/test/java/com/ditto/example/spring/quickstart/TaskVisibilityIntegrationTest.java @@ -3,8 +3,17 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.github.bonigarcia.wdm.WebDriverManager; -import org.junit.jupiter.api.*; -import org.openqa.selenium.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.Timeout; +import org.openqa.selenium.By; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.RemoteWebDriver; @@ -143,8 +152,8 @@ void shouldPassWithExistingTask() { wait.until(ExpectedConditions.titleContains("Ditto")); wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("input[placeholder*='Task']"))); - // Enable sync if disabled - enableSyncIfDisabled(); + // Sync must be enabled out of the box — assert it rather than repairing the state. + assertSyncEnabledOnLoad(); // Wait for tasks to load try { @@ -178,28 +187,17 @@ private boolean isTaskVisibleOnPage(String taskTitle) { } } - private void enableSyncIfDisabled() { + private void assertSyncEnabledOnLoad() { + // The app must sync out of the box: once the real state is streamed in, the + // control reports "Sync State: Enabled" without any interaction. This also + // proves the rendered control reflects the actual state (the initial HTML is + // a neutral placeholder, not a hard-coded state). Do NOT repair the state + // here — if sync starts disabled this must fail. try { - // Use CSS selector for more reliable element location - List allElements = driver.findElements(By.cssSelector("*")); - - for (WebElement element : allElements) { - String text = element.getText(); - if (text.contains("Sync State: false")) { - // Find toggle button using CSS selector instead of XPath - List buttons = driver.findElements(By.cssSelector("button")); - for (WebElement button : buttons) { - if ("Toggle".equals(button.getText().trim())) { - button.click(); - Thread.sleep(2000); - return; - } - } - break; - } - } - } catch (Exception e) { - System.err.println("Warning: Could not enable sync: " + e.getMessage()); + wait.until(d -> d.getPageSource().contains("Sync State: Enabled")); + } catch (TimeoutException e) { + Assertions.fail("Sync should be enabled on initial load (\"Sync State: Enabled\"), " + + "but it was not. The quickstart must sync out of the box."); } } diff --git a/javascript-tui/.prettierignore b/javascript-tui/.prettierignore index 1521c8b76..67af58370 100644 --- a/javascript-tui/.prettierignore +++ b/javascript-tui/.prettierignore @@ -1 +1,2 @@ dist +package-lock.json diff --git a/javascript-tui/README.md b/javascript-tui/README.md index 41cc1c0b5..c54e45f78 100644 --- a/javascript-tui/README.md +++ b/javascript-tui/README.md @@ -5,7 +5,7 @@ This app is a TUI built using [Ink](https://github.com/vadimdemedes/ink) with Re ## Documentation - [Javascript Install Guide](https://docs.ditto.live/sdk/latest/install-guides/js) -- [Javascript API Reference](https://software.ditto.live/js/Ditto/5.0.0/api-reference/) +- [Javascript API Reference](https://docs.ditto.live/sdk/latest/api-reference/js) - [Javascript Release Notes](https://docs.ditto.live/sdk/latest/release-notes/js) ## Prerequisites @@ -16,17 +16,17 @@ This app is a TUI built using [Ink](https://github.com/vadimdemedes/ink) with Re ## Getting Started First, in the root of this repository, copy the `.env.sample` file to `.env`, -then fill out the variables with your Ditto AppID, Auth URL, Playground Token, -and Websocket URL. If you don't have those yet, visit https://portal.ditto.live +then fill out the variables with your Ditto Database ID, Server URL, and +Development Token. If you don't have those yet, visit https://portal.ditto.live ```bash cp .env.sample .env ``` ``` -DITTO_APP_ID="" -DITTO_PLAYGROUND_TOKEN="" -DITTO_AUTH_URL="" +DITTO_DATABASE_ID="" +DITTO_DEVELOPMENT_TOKEN="" +DITTO_SERVER_URL="" ``` Next, install dependencies and run: @@ -65,18 +65,18 @@ npm start 2>NUL ## Architecture -This app uses the Ditto v5 SDK in **server mode** with the Online Playground -identity (development only). It connects to the Ditto cloud via WebSocket and -syncs a `tasks` collection. +This app uses the Ditto v5 SDK in **server mode** with the Development +identity (development only). It connects to the Ditto server and syncs a +`tasks` collection. Each task document has the following structure: ```json { - "_id": "unique-id", - "title": "Task description", - "done": false, - "deleted": false + "_id": "unique-id", + "title": "Task description", + "done": false, + "deleted": false } ``` diff --git a/javascript-tui/package-lock.json b/javascript-tui/package-lock.json index 7693f4f44..a1e0419d6 100644 --- a/javascript-tui/package-lock.json +++ b/javascript-tui/package-lock.json @@ -1,6919 +1,6923 @@ { - "name": "javascript", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "javascript", - "version": "0.0.0", - "license": "MIT", - "dependencies": { - "@dittolive/ditto": "5.0.0", - "dotenv": "^16.4.5", - "ink": "^4.1.0", - "meow": "^11.0.0", - "react": "^18.2.0", - "tempy": "^3.1.0" - }, - "bin": { - "javascript": "dist/cli.js" - }, - "devDependencies": { - "@babel/cli": "^7.21.0", - "@babel/preset-env": "^7.28.3", - "@babel/preset-react": "^7.18.6", - "@vdemedes/prettier-config": "^2.0.1", - "eslint": "^9.18.0", - "eslint-config-xo-react": "^0.30.1", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.1.1", - "ink-testing-library": "^4.0.0", - "prettier": "^2.8.7" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@alcalzone/ansi-tokenize": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", - "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=14.13.1" - } - }, - "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@babel/cli": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.28.6.tgz", - "integrity": "sha512-6EUNcuBbNkj08Oj4gAZ+BUU8yLCgKzgVX4gaTh09Ya2C8ICM4P+G30g4m3akRxSYAp3A/gnWchrNst7px4/nUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.28", - "commander": "^6.2.0", - "convert-source-map": "^2.0.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.2.0", - "make-dir": "^2.1.0", - "slash": "^2.0.0" - }, - "bin": { - "babel": "bin/babel.js", - "babel-external-helpers": "bin/babel-external-helpers.js" - }, - "engines": { - "node": ">=6.9.0" - }, - "optionalDependencies": { - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", - "chokidar": "^3.6.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", - "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.29.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "regexpu-core": "^6.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", - "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "debug": "^4.4.3", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.11" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz", - "integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", - "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", - "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", - "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-syntax-jsx": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.29.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz", - "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.28.6", - "@babel/plugin-syntax-import-attributes": "^7.28.6", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.29.0", - "@babel/plugin-transform-async-to-generator": "^7.28.6", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.6", - "@babel/plugin-transform-class-properties": "^7.28.6", - "@babel/plugin-transform-class-static-block": "^7.28.6", - "@babel/plugin-transform-classes": "^7.28.6", - "@babel/plugin-transform-computed-properties": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.28.6", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.6", - "@babel/plugin-transform-exponentiation-operator": "^7.28.6", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.28.6", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.4", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", - "@babel/plugin-transform-numeric-separator": "^7.28.6", - "@babel/plugin-transform-object-rest-spread": "^7.28.6", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.28.6", - "@babel/plugin-transform-optional-chaining": "^7.28.6", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.28.6", - "@babel/plugin-transform-private-property-in-object": "^7.28.6", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.29.0", - "@babel/plugin-transform-regexp-modifiers": "^7.28.6", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.28.6", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.28.6", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.15", - "babel-plugin-polyfill-corejs3": "^0.14.0", - "babel-plugin-polyfill-regenerator": "^0.6.6", - "core-js-compat": "^3.48.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", - "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.28.0", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@dittolive/ditto": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@dittolive/ditto/-/ditto-5.0.0.tgz", - "integrity": "sha512-Qv4bXhaXbCJlazFa2nLsx6LLeFohBuT0GupfXo7R+nalNGv5kzrJMuafbawydIuaP7ZgGn2e+39eiSny8iRSHg==", - "license": "SEE LICENSE IN LICENSE.md", - "engines": { - "node": ">=14" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "license": "MIT" - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "license": "MIT" - }, - "node_modules/@vdemedes/prettier-config": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@vdemedes/prettier-config/-/prettier-config-2.0.1.tgz", - "integrity": "sha512-lcHyyLfS2ro282qsXKpxw+canUkOlFIGoanxt3BaNCm5K1NR8k4hGvYbFO54/+QWq12d0y/EYRz68yNQkqWFrw==", - "dev": true, - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", - "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/auto-bind": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", - "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", - "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.8", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", - "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8", - "core-js-compat": "^3.48.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", - "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.27", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", - "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-8.0.2.tgz", - "integrity": "sha512-qMKdlOfsjlezMqxkUGGMaWWs17i2HoL15tM+wtx8ld4nLrUwU58TFdvyGOz/piNP842KeO8yXvggVQSdQ828NA==", - "license": "MIT", - "dependencies": { - "camelcase": "^7.0.0", - "map-obj": "^4.3.0", - "quick-lru": "^6.1.1", - "type-fest": "^2.13.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001791", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", - "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/code-excerpt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", - "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", - "license": "MIT", - "dependencies": { - "convert-to-spaces": "^2.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-to-spaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", - "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/core-js-compat": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", - "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "license": "MIT", - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", - "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "license": "MIT", - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.349", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", - "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", - "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", - "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.2", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-xo-react": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/eslint-config-xo-react/-/eslint-config-xo-react-0.30.1.tgz", - "integrity": "sha512-oiAdOnjyIyS5/uJsVHeeLR5P2e8rkiA+NVYLi0UC9Ulcrehdg3wMj4bPmPK/ogkypxGi0DAw2SBD4VoVxJu9ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1" - }, - "engines": { - "node": ">=18.18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "peerDependencies": { - "eslint": ">=9.18.0" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/hosted-git-info": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", - "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", - "license": "ISC", - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ink": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/ink/-/ink-4.4.1.tgz", - "integrity": "sha512-rXckvqPBB0Krifk5rn/5LvQGmyXwCUpBfmTwbkQNBY9JY8RSl3b8OftBNEYxg4+SWUhEKcPifgope28uL9inlA==", - "license": "MIT", - "dependencies": { - "@alcalzone/ansi-tokenize": "^0.1.3", - "ansi-escapes": "^6.0.0", - "auto-bind": "^5.0.1", - "chalk": "^5.2.0", - "cli-boxes": "^3.0.0", - "cli-cursor": "^4.0.0", - "cli-truncate": "^3.1.0", - "code-excerpt": "^4.0.0", - "indent-string": "^5.0.0", - "is-ci": "^3.0.1", - "is-lower-case": "^2.0.2", - "is-upper-case": "^2.0.2", - "lodash": "^4.17.21", - "patch-console": "^2.0.0", - "react-reconciler": "^0.29.0", - "scheduler": "^0.23.0", - "signal-exit": "^3.0.7", - "slice-ansi": "^6.0.0", - "stack-utils": "^2.0.6", - "string-width": "^5.1.2", - "type-fest": "^0.12.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.1.0", - "ws": "^8.12.0", - "yoga-wasm-web": "~0.3.3" - }, - "engines": { - "node": ">=14.16" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "react": ">=18.0.0", - "react-devtools-core": "^4.19.1" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react-devtools-core": { - "optional": true - } - } - }, - "node_modules/ink-testing-library": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-4.0.0.tgz", - "integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/react": ">=18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/ink/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz", - "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", - "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/meow": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-11.0.0.tgz", - "integrity": "sha512-Cl0yeeIrko6d94KpUo1M+0X1sB14ikoaqlIGuTH1fW4I+E3+YljL54/hb/BWmVfrV9tTV9zU04+xjw08Fh2WkA==", - "license": "MIT", - "dependencies": { - "@types/minimist": "^1.2.2", - "camelcase-keys": "^8.0.2", - "decamelize": "^6.0.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^4.0.1", - "read-pkg-up": "^9.1.0", - "redent": "^4.0.0", - "trim-newlines": "^4.0.2", - "type-fest": "^3.1.0", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "license": "MIT", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-package-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", - "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^5.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/patch-console": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", - "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/quick-lru": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", - "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-reconciler": { - "version": "0.29.2", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", - "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/read-pkg": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", - "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^2.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", - "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", - "license": "MIT", - "dependencies": { - "find-up": "^6.3.0", - "read-pkg": "^7.1.0", - "type-fest": "^2.5.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "license": "MIT", - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/read-pkg/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", - "license": "MIT", - "dependencies": { - "indent-string": "^5.0.0", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", - "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", - "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "get-intrinsic": "^1.3.0", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/slice-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-6.0.0.tgz", - "integrity": "sha512-6bn4hRfkTvDfUoEQYkERg0BVF1D0vrX9HEkMl08uDiNWvVvjylLHvZFZWkDo6wjT8tUctbYl1nCOuE66ZTaUtA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", - "license": "CC0-1.0" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-indent": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", - "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/temp-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", - "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/tempy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.2.0.tgz", - "integrity": "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==", - "license": "MIT", - "dependencies": { - "is-stream": "^3.0.0", - "temp-dir": "^3.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/trim-newlines": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", - "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", - "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "license": "MIT", - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "license": "MIT", - "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoga-wasm-web": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", - "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", - "license": "MIT" - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } + "name": "javascript", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "javascript", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@dittolive/ditto": "5.0.3", + "dotenv": "^16.4.5", + "ink": "^4.1.0", + "meow": "^11.0.0", + "react": "^18.2.0", + "tempy": "^3.1.0" + }, + "bin": { + "javascript": "dist/cli.js" + }, + "devDependencies": { + "@babel/cli": "^7.21.0", + "@babel/preset-env": "^7.28.3", + "@babel/preset-react": "^7.18.6", + "eslint": "^9.18.0", + "eslint-config-xo-react": "^0.30.1", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "ink-testing-library": "^4.0.0", + "prettier": "^2.8.7" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@babel/cli": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.28.6.tgz", + "integrity": "sha512-6EUNcuBbNkj08Oj4gAZ+BUU8yLCgKzgVX4gaTh09Ya2C8ICM4P+G30g4m3akRxSYAp3A/gnWchrNst7px4/nUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.28", + "commander": "^6.2.0", + "convert-source-map": "^2.0.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "bin": { + "babel": "bin/babel.js", + "babel-external-helpers": "bin/babel-external-helpers.js" + }, + "engines": { + "node": ">=6.9.0" + }, + "optionalDependencies": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.6.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", + "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.29.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz", + "integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz", + "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.4", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dittolive/ditto": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@dittolive/ditto/-/ditto-5.0.3.tgz", + "integrity": "sha512-CDz3kRhSvBPsq95HUcIdA0CONFUXU5cEfZ0r60k++y6lmjgReJMLtimDfGoUp2YQufuYZpfmcXNp1x/OITBcMQ==", + "license": "SEE LICENSE IN LICENSE.md", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@expo/config-plugins": "*", + "expo-build-properties": "*" + }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + }, + "expo-build-properties": { + "optional": true + } + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "license": "MIT" + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", + "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-8.0.2.tgz", + "integrity": "sha512-qMKdlOfsjlezMqxkUGGMaWWs17i2HoL15tM+wtx8ld4nLrUwU58TFdvyGOz/piNP842KeO8yXvggVQSdQ828NA==", + "license": "MIT", + "dependencies": { + "camelcase": "^7.0.0", + "map-obj": "^4.3.0", + "quick-lru": "^6.1.1", + "type-fest": "^2.13.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-xo-react": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/eslint-config-xo-react/-/eslint-config-xo-react-0.30.1.tgz", + "integrity": "sha512-oiAdOnjyIyS5/uJsVHeeLR5P2e8rkiA+NVYLi0UC9Ulcrehdg3wMj4bPmPK/ogkypxGi0DAw2SBD4VoVxJu9ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1" + }, + "engines": { + "node": ">=18.18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "peerDependencies": { + "eslint": ">=9.18.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hosted-git-info": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", + "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-4.4.1.tgz", + "integrity": "sha512-rXckvqPBB0Krifk5rn/5LvQGmyXwCUpBfmTwbkQNBY9JY8RSl3b8OftBNEYxg4+SWUhEKcPifgope28uL9inlA==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^6.0.0", + "auto-bind": "^5.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^3.1.0", + "code-excerpt": "^4.0.0", + "indent-string": "^5.0.0", + "is-ci": "^3.0.1", + "is-lower-case": "^2.0.2", + "is-upper-case": "^2.0.2", + "lodash": "^4.17.21", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^6.0.0", + "stack-utils": "^2.0.6", + "string-width": "^5.1.2", + "type-fest": "^0.12.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0", + "ws": "^8.12.0", + "yoga-wasm-web": "~0.3.3" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-testing-library": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-4.0.0.tgz", + "integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/ink/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz", + "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", + "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/meow": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-11.0.0.tgz", + "integrity": "sha512-Cl0yeeIrko6d94KpUo1M+0X1sB14ikoaqlIGuTH1fW4I+E3+YljL54/hb/BWmVfrV9tTV9zU04+xjw08Fh2WkA==", + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.2", + "camelcase-keys": "^8.0.2", + "decamelize": "^6.0.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^4.0.1", + "read-pkg-up": "^9.1.0", + "redent": "^4.0.0", + "trim-newlines": "^4.0.2", + "type-fest": "^3.1.0", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", + "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^5.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/read-pkg": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", + "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^2.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", + "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0", + "read-pkg": "^7.1.0", + "type-fest": "^2.5.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/read-pkg/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "license": "MIT", + "dependencies": { + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-6.0.0.tgz", + "integrity": "sha512-6bn4hRfkTvDfUoEQYkERg0BVF1D0vrX9HEkMl08uDiNWvVvjylLHvZFZWkDo6wjT8tUctbYl1nCOuE66ZTaUtA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "license": "CC0-1.0" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.2.0.tgz", + "integrity": "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==", + "license": "MIT", + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-newlines": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", + "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", + "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-wasm-web": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } } diff --git a/javascript-tui/package.json b/javascript-tui/package.json index ec3fbb81e..d997ce07d 100644 --- a/javascript-tui/package.json +++ b/javascript-tui/package.json @@ -1,68 +1,66 @@ { - "name": "javascript", - "version": "0.0.0", - "license": "MIT", - "bin": "dist/cli.js", - "type": "module", - "engines": { - "node": ">=20" - }, - "scripts": { - "start": "npm run build && node dist/cli.js", - "build": "babel --out-dir=dist source", - "dev": "babel --out-dir=dist --watch source", - "test": "npm run test:format && npm run test:integration", - "test:format": "prettier --check .", - "test:integration": "npm run build && DITTO_CLOUD_TASK_TITLE=\"\" node tests/integration_test.js", - "format": "prettier --write ." - }, - "files": [ - "dist" - ], - "dependencies": { - "@dittolive/ditto": "5.0.0", - "dotenv": "^16.4.5", - "ink": "^4.1.0", - "meow": "^11.0.0", - "react": "^18.2.0", - "tempy": "^3.1.0" - }, - "devDependencies": { - "@babel/cli": "^7.21.0", - "@babel/preset-env": "^7.28.3", - "@babel/preset-react": "^7.18.6", - "@vdemedes/prettier-config": "^2.0.1", - "eslint": "^9.18.0", - "eslint-config-xo-react": "^0.30.1", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.1.1", - "ink-testing-library": "^4.0.0", - "prettier": "^2.8.7" - }, - "overrides": { - "tar": "7.5.11", - "undici": "6.24.1", - "flatted": "3.4.2", - "lodash": "4.18.1", - "glob": "^13.0.6" - }, - "xo": { - "extends": "xo-react", - "prettier": true, - "rules": { - "react/prop-types": "off" - } - }, - "prettier": "@vdemedes/prettier-config", - "babel": { - "presets": [ - [ - "@babel/preset-env", - { - "modules": false - } - ], - "@babel/preset-react" - ] - } + "name": "javascript", + "version": "0.0.0", + "license": "MIT", + "bin": "dist/cli.js", + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "start": "npm run build && node dist/cli.js", + "build": "babel --out-dir=dist source", + "dev": "babel --out-dir=dist --watch source", + "test": "npm run test:format && npm run test:integration", + "test:format": "prettier --check .", + "test:integration": "npm run build && DITTO_CLOUD_TASK_TITLE=\"\" node tests/integration_test.js", + "format": "prettier --write ." + }, + "files": [ + "dist" + ], + "dependencies": { + "@dittolive/ditto": "5.0.3", + "dotenv": "^16.4.5", + "ink": "^4.1.0", + "meow": "^11.0.0", + "react": "^18.2.0", + "tempy": "^3.1.0" + }, + "devDependencies": { + "@babel/cli": "^7.21.0", + "@babel/preset-env": "^7.28.3", + "@babel/preset-react": "^7.18.6", + "eslint": "^9.18.0", + "eslint-config-xo-react": "^0.30.1", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "ink-testing-library": "^4.0.0", + "prettier": "^2.8.7" + }, + "overrides": { + "tar": "7.5.11", + "undici": "6.24.1", + "flatted": "3.4.2", + "lodash": "4.18.1", + "glob": "^13.0.6" + }, + "xo": { + "extends": "xo-react", + "prettier": true, + "rules": { + "react/prop-types": "off" + } + }, + "babel": { + "presets": [ + [ + "@babel/preset-env", + { + "modules": false + } + ], + "@babel/preset-react" + ] + } } diff --git a/javascript-tui/prettier.config.cjs b/javascript-tui/prettier.config.cjs new file mode 100644 index 000000000..c3ba00a99 --- /dev/null +++ b/javascript-tui/prettier.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + singleQuote: true, + trailingComma: 'all', + tabWidth: 2, + semi: true, + printWidth: 80, +}; diff --git a/javascript-tui/prettier.config.js b/javascript-tui/prettier.config.js deleted file mode 100644 index d39a4a367..000000000 --- a/javascript-tui/prettier.config.js +++ /dev/null @@ -1,9 +0,0 @@ -const config = { - singleQuote: true, - trailingComma: 'all', - tabWidth: 2, - semi: true, - printWidth: 80, -}; - -export default config; diff --git a/javascript-tui/source/app.js b/javascript-tui/source/app.js index 14c6e9844..bdd8cc0b0 100644 --- a/javascript-tui/source/app.js +++ b/javascript-tui/source/app.js @@ -1,331 +1,277 @@ -import React, {useEffect, useRef, useState} from 'react'; -import {Text, Spacer, Box, useInput} from 'ink'; +import React, { useEffect, useRef, useState } from 'react'; +import { Text, Spacer, Box, useInput } from 'ink'; +import { TasksRepository } from './tasks-repository.js'; const enterAltScreenCommand = '\x1b[?1049h'; const leaveAltScreenCommand = '\x1b[?1049l'; -export default function App({ditto}) { - return ( - - - - - - ); +export default function App({ ditto, dittoManager }) { + return ( + + + + + + ); } // Helper component to make TUI fullscreen and wrap with a border -const FullScreen = props => { - const [size, setSize] = useState({ - columns: process.stdout.columns, - rows: process.stdout.rows, - }); - - useEffect(() => { - function onResize() { - setSize({ - columns: process.stdout.columns, - rows: process.stdout.rows - 1, - }); - } - - process.stdout.on('resize', onResize); - process.stdout.write(enterAltScreenCommand); - return () => { - process.stdout.off('resize', onResize); - process.stdout.write(leaveAltScreenCommand); - }; - }, []); - - return ( - - {props.children} - - ); +const FullScreen = (props) => { + const [size, setSize] = useState({ + columns: process.stdout.columns, + rows: process.stdout.rows, + }); + + useEffect(() => { + function onResize() { + setSize({ + columns: process.stdout.columns, + rows: process.stdout.rows - 1, + }); + } + + process.stdout.on('resize', onResize); + process.stdout.write(enterAltScreenCommand); + return () => { + process.stdout.off('resize', onResize); + process.stdout.write(leaveAltScreenCommand); + }; + }, []); + + return ( + + {props.children} + + ); }; -const HelpPanel = props => { - const [showHelp, setShowHelp] = useState(true); - - useInput((input, _key) => { - if (input === '?') { - setShowHelp(!showHelp); - } - }); - - if (showHelp) { - return ( - <> - - {props.children} - - - ? - toggle help - ↑/k - scroll up - ↓/j - scroll down - c - create task - d - delete task - e - edit task - s - toggle sync - q - quit - Enter - toggle done - - - - ); - } - - return <>{props.children}; +const HelpPanel = (props) => { + const [showHelp, setShowHelp] = useState(true); + + useInput((input, _key) => { + if (input === '?') { + setShowHelp(!showHelp); + } + }); + + if (showHelp) { + return ( + <> + + {props.children} + + + ? - toggle help + ↑/k - scroll up + ↓/j - scroll down + c - create task + d - delete task + e - edit task + s - toggle sync + q - quit + Enter - toggle done + + + + ); + } + + return <>{props.children}; }; const LIST_MODE = 'list'; const CREATE_MODE = 'create'; const EDIT_MODE = 'edit'; -const TodoApp = ({ditto}) => { - const [tasks, setTasks] = useState([]); - const [mode, setMode] = useState(LIST_MODE); - const [selected, setSelected] = useState(0); - const [syncEnabled, setSyncEnabled] = useState(true); - - // Hold onto the subscription and observer refs for cleanup - const subscriptionRef = useRef(null); - const observerRef = useRef(null); - - useEffect(() => { - return () => { - const subscription = subscriptionRef.current; - const observer = observerRef.current; - - void subscription; - void observer; - }; - }, []); - - useInput((input, key) => { - if (mode === LIST_MODE) { - if (input === 'c') { - setMode(CREATE_MODE); - return; - } - if (input === 'e') { - setMode(EDIT_MODE); - return; - } - if (input === 's') { - if (syncEnabled) { - ditto.sync.stop(); - setSyncEnabled(false); - } else { - ditto.sync.start(); - setSyncEnabled(true); - } - } - } - - if (key.escape) { - setMode(LIST_MODE); - return; - } - }); - - useEffect(() => { - // Register a subscription, which determines what data syncs to this peer - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - const subscription = ditto.sync.registerSubscription('SELECT * FROM tasks'); - subscriptionRef.current = subscription; - - // Register observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - const observer = ditto.store.registerObserver( - 'SELECT * FROM tasks WHERE NOT deleted ORDER BY title ASC', - result => { - const tasks = result.items.map(item => item.value); - setTasks(tasks); - }, - ); - observerRef.current = observer; - - // Cleanup on unmount: cancel subscription and observer - return () => { - subscription.cancel(); - observer.cancel(); - }; - }, [ditto]); - - const Prompt = React.memo(({edit}) => { - const newTask = !edit; - const initialText = newTask ? '' : edit.title; - const [text, setText] = useState(initialText); - - useInput((input, key) => { - if (key.backspace || key.delete) { - // Chop off last char and set - setText(text.slice(0, -1)); - return; - } - - if (key.return) { - if (newTask) { - (async () => { - await createTask(ditto, text); - })(); - } else { - (async () => { - await updateTask(ditto, edit._id, text); - })(); - } - - // On submission: - setText(''); // Reset input field - setMode(LIST_MODE); // Set app back to "list" mode - return; - } - - const newContent = text + input; - setText(newContent); - }); - - return Title: {text}; - }); - - const List = React.memo(({tasks}) => { - useInput((input, key) => { - // Scroll up - if (input === 'k' || key.upArrow) { - if (selected > 0) { - setSelected(selected - 1); - } - return; - } - - // Scroll down - if (input === 'j' || key.downArrow) { - if (selected < tasks.length - 1) { - setSelected(selected + 1); - } - return; - } - - // Delete - if (input === 'd') { - if (tasks.length > 0) { - (async () => { - await deleteTask(ditto, tasks[selected]); - })(); - } - } - - // Quit - if (input === 'q') { - process.stdout.write('\x1B[?25h'); // Make cursor visible - process.exit(0); - } - - if (key.return) { - (async () => { - await toggleDone(ditto, tasks[selected]); - })(); - } - }); - - return ( - - {tasks.map((task, i) => { - const done = task.done ? ' 🟢 ' : ' āšŖļø '; - const highlight = selected === i ? 'blue' : ''; - const cursor = selected === i ? 'āÆ ' : ' '; - return ( - - - {done} - - {cursor} - {task.title} - - - - ); - })} - - ); - }); - - const syncStatus = syncEnabled ? '🟢 Sync Active' : 'šŸ”“ Sync Inactive'; - const syncText = {syncStatus}; - - if (mode === LIST_MODE) { - return ( - - {syncText} - Done Title - - - ); - } - - if (mode === CREATE_MODE) { - return ( - - {syncText} - Create new Task - - - ); - } - - if (mode === EDIT_MODE) { - const selectedTask = tasks[selected]; - return ( - - {syncText} - Edit Task - - - ); - } -}; - -// https://docs.ditto.live/sdk/latest/crud/update -const toggleDone = async (ditto, task) => { - await ditto.store.execute('UPDATE tasks SET done=:done WHERE _id=:id', { - id: task._id, - done: !task.done, - }); -}; - -// https://docs.ditto.live/sdk/latest/crud/create -const createTask = async (ditto, title) => { - await ditto.store.execute('INSERT INTO tasks DOCUMENTS (:task)', { - task: { - title, - done: false, - deleted: false, - }, - }); -}; - -// https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern -const deleteTask = async (ditto, task) => { - await ditto.store.execute('UPDATE tasks SET deleted=true WHERE _id=:id', { - id: task._id, - }); -}; - -// https://docs.ditto.live/sdk/latest/crud/update -const updateTask = async (ditto, id, title) => { - await ditto.store.execute('UPDATE tasks SET title=:title WHERE _id=:id', { - id, - title, - }); +const TodoApp = ({ ditto, dittoManager }) => { + const [tasks, setTasks] = useState([]); + const [mode, setMode] = useState(LIST_MODE); + const [selected, setSelected] = useState(0); + const [syncEnabled, setSyncEnabled] = useState(true); + + // Hold onto the tasks repository so the input handlers can drive CRUD. + const repositoryRef = useRef(null); + + useInput((input, key) => { + if (mode === LIST_MODE) { + if (input === 'c') { + setMode(CREATE_MODE); + return; + } + if (input === 'e') { + setMode(EDIT_MODE); + return; + } + if (input === 's') { + if (syncEnabled) { + dittoManager.stopSync(); + setSyncEnabled(false); + } else { + dittoManager.startSync(); + setSyncEnabled(true); + } + } + } + + if (key.escape) { + setMode(LIST_MODE); + return; + } + }); + + useEffect(() => { + // The repository registers the tasks subscription + observer and streams + // the observed task list back into local state. + const repository = new TasksRepository(ditto, setTasks); + repository.start(); + repositoryRef.current = repository; + + // Cleanup on unmount: cancel subscription and observer + return () => { + repository.dispose(); + repositoryRef.current = null; + }; + }, [ditto]); + + const Prompt = React.memo(({ edit }) => { + const newTask = !edit; + const initialText = newTask ? '' : edit.title; + const [text, setText] = useState(initialText); + + useInput((input, key) => { + if (key.backspace || key.delete) { + // Chop off last char and set + setText(text.slice(0, -1)); + return; + } + + if (key.return) { + if (newTask) { + (async () => { + await repositoryRef.current.createTask(text); + })(); + } else { + (async () => { + await repositoryRef.current.editTask(edit._id, text); + })(); + } + + // On submission: + setText(''); // Reset input field + setMode(LIST_MODE); // Set app back to "list" mode + return; + } + + const newContent = text + input; + setText(newContent); + }); + + return Title: {text}; + }); + + const List = React.memo(({ tasks }) => { + useInput((input, key) => { + // Scroll up + if (input === 'k' || key.upArrow) { + if (selected > 0) { + setSelected(selected - 1); + } + return; + } + + // Scroll down + if (input === 'j' || key.downArrow) { + if (selected < tasks.length - 1) { + setSelected(selected + 1); + } + return; + } + + // Delete + if (input === 'd') { + if (tasks.length > 0) { + (async () => { + await repositoryRef.current.deleteTask(tasks[selected]); + })(); + } + } + + // Quit + if (input === 'q') { + process.stdout.write('\x1B[?25h'); // Make cursor visible + process.exit(0); + } + + if (key.return) { + (async () => { + await repositoryRef.current.toggleTask(tasks[selected]); + })(); + } + }); + + return ( + + {tasks.map((task, i) => { + const done = task.done ? ' 🟢 ' : ' āšŖļø '; + const highlight = selected === i ? 'blue' : ''; + const cursor = selected === i ? 'āÆ ' : ' '; + return ( + + + {done} + + {cursor} + {task.title} + + + + ); + })} + + ); + }); + + const syncStatus = syncEnabled ? '🟢 Sync Active' : 'šŸ”“ Sync Inactive'; + const syncText = {syncStatus}; + + if (mode === LIST_MODE) { + return ( + + {syncText} + Done Title + + + ); + } + + if (mode === CREATE_MODE) { + return ( + + {syncText} + Create new Task + + + ); + } + + if (mode === EDIT_MODE) { + const selectedTask = tasks[selected]; + return ( + + {syncText} + Edit Task + + + ); + } }; diff --git a/javascript-tui/source/cli.js b/javascript-tui/source/cli.js index 2db1e48a8..d1fa0ea9c 100644 --- a/javascript-tui/source/cli.js +++ b/javascript-tui/source/cli.js @@ -1,41 +1,37 @@ #!/usr/bin/env node import React from 'react'; -import {render} from 'ink'; +import { render } from 'ink'; import meow from 'meow'; import App from './app.js'; import dotenv from 'dotenv'; -import {Ditto, DittoConfig, Authenticator} from '@dittolive/ditto'; -import {temporaryDirectory} from 'tempy'; +import { temporaryDirectory } from 'tempy'; +import { DittoManager } from './ditto-manager.js'; -dotenv.config({path: '../.env'}); +dotenv.config({ path: '../.env' }); const cli = meow( - ` + ` Usage $ npm start -- 2>/dev/null Options - --app-id [env: DITTO_APP_ID] Your Ditto AppID - --playground-token [env: DITTO_PLAYGROUND_TOKEN] An OnlinePlayground token - --auth-url [env: DITTO_AUTH_URL] The auth URL - --websocket-url [env: DITTO_WEBSOCKET_URL] The websocket URL + --database-id [env: DITTO_DATABASE_ID] Your Ditto Database ID + --development-token [env: DITTO_DEVELOPMENT_TOKEN] A Development token + --server-url [env: DITTO_SERVER_URL] The server URL `, - { - importMeta: import.meta, - flags: { - appId: { - type: 'string', - }, - playgroundToken: { - type: 'string', - }, - authURL: { - type: 'string', - }, - websocketURL: { - type: 'string', - }, - }, - }, + { + importMeta: import.meta, + flags: { + databaseId: { + type: 'string', + }, + developmentToken: { + type: 'string', + }, + serverURL: { + type: 'string', + }, + }, + }, ); // We use a temporary directory to store Ditto's local database. This @@ -47,81 +43,31 @@ const cli = meow( // instance has its own persistence directory. const tempdir = temporaryDirectory(); -// Grab appID and token from CLI or .env in that order -const appID = cli.flags.appId ?? process.env.DITTO_APP_ID; -const token = cli.flags.playgroundToken ?? process.env.DITTO_PLAYGROUND_TOKEN; -const authURL = cli.flags.authURL ?? process.env.DITTO_AUTH_URL; -const websocketURL = cli.flags.websocketURL ?? process.env.DITTO_WEBSOCKET_URL; +// Grab database ID and token from CLI or .env in that order +const databaseId = cli.flags.databaseId ?? process.env.DITTO_DATABASE_ID; +const token = cli.flags.developmentToken ?? process.env.DITTO_DEVELOPMENT_TOKEN; +const serverURL = cli.flags.serverURL ?? process.env.DITTO_SERVER_URL; -// Create a new Ditto instance with the DittoConfig +// Open a configured, already-running Ditto instance via the DittoManager, +// which owns instance/identity/transport/sync setup. // https://docs.ditto.live/sdk/latest/install-guides/nodejs#installing-the-demo-task-app -const connectConfig = { - mode: 'server', - url: authURL, -}; - -const config = new DittoConfig(appID, connectConfig, tempdir); -const ditto = await Ditto.open(config); - -// Initialize transport config — enable LAN P2P and WebSocket. -// BLE and AWDL are disabled because they require macOS entitlements -// that are only available to signed app bundles, not Node.js processes. -// LAN (TCP + mDNS) provides P2P sync with peers on the local network. -ditto.updateTransportConfig(config => { - config.peerToPeer.bluetoothLE.isEnabled = false; - config.peerToPeer.awdl.isEnabled = false; - config.peerToPeer.lan.isEnabled = true; - config.peerToPeer.lan.isMdnsEnabled = true; - config.peerToPeer.lan.isMulticastEnabled = true; +const dittoManager = new DittoManager({ + databaseId, + token, + serverURL, + persistenceDirectory: tempdir, }); +const ditto = await dittoManager.open(); -// Set up authentication for server mode -if (connectConfig.mode === 'server') { - await ditto.auth.setExpirationHandler( - async (dittoInstance, timeUntilExpiration) => { - console.log( - 'Authentication expiring soon, time until expiration:', - timeUntilExpiration, - ); - - if (dittoInstance.auth.loginSupported) { - const devProvider = Authenticator.DEVELOPMENT_PROVIDER; - const reLoginResult = await dittoInstance.auth.login( - token, - devProvider, - ); - if (reLoginResult.error) { - console.error('Re-authentication failed:', reLoginResult.error); - } else { - console.log( - 'Successfully re-authenticated with info:', - reLoginResult, - ); - } - } - }, - ); - - if (ditto.auth.loginSupported) { - const devProvider = Authenticator.DEVELOPMENT_PROVIDER; - const loginResult = await ditto.auth.login(token, devProvider); - if (loginResult.error) { - console.error('Login failed:', loginResult.error); - } else { - console.log('Successfully logged in with info:', loginResult); - } - } -} - -ditto.sync.start(); - -process.on('uncaughtException', err => { - console.error('Uncaught Exception:', err); +process.on('uncaughtException', (err) => { + console.error('Uncaught Exception:', err); }); -process.on('unhandledRejection', reason => { - console.error('Unhandled Rejection:', reason); +process.on('unhandledRejection', (reason) => { + console.error('Unhandled Rejection:', reason); }); -const {waitUntilExit} = render(); +const { waitUntilExit } = render( + , +); await waitUntilExit(); diff --git a/javascript-tui/source/ditto-manager.js b/javascript-tui/source/ditto-manager.js new file mode 100644 index 000000000..c098491ad --- /dev/null +++ b/javascript-tui/source/ditto-manager.js @@ -0,0 +1,92 @@ +import { Ditto, DittoConfig, Authenticator } from '@dittolive/ditto'; + +// Owns the Ditto instance lifecycle: config, open, identity/auth, transport, +// and sync start/stop. Vends a configured, already-running instance and holds +// onto it. Knows nothing about tasks. +export class DittoManager { + constructor({ databaseId, token, serverURL, persistenceDirectory }) { + this.databaseId = databaseId; + this.token = token; + this.serverURL = serverURL; + this.persistenceDirectory = persistenceDirectory; + this.ditto = null; + } + + // Opens a Ditto instance, authenticates, configures transports, starts sync, + // and returns the ready instance. + // https://docs.ditto.live/sdk/latest/install-guides/nodejs#installing-the-demo-task-app + async open() { + const connectConfig = { + mode: 'server', + url: this.serverURL, + }; + + const config = new DittoConfig( + this.databaseId, + connectConfig, + this.persistenceDirectory, + ); + const ditto = await Ditto.open(config); + this.ditto = ditto; + + // Initialize transport config — enable LAN P2P. + // BLE and AWDL are disabled because they require macOS entitlements + // that are only available to signed app bundles, not Node.js processes. + // LAN (TCP + mDNS) provides P2P sync with peers on the local network. + ditto.updateTransportConfig((config) => { + config.peerToPeer.bluetoothLE.isEnabled = false; + config.peerToPeer.awdl.isEnabled = false; + config.peerToPeer.lan.isEnabled = true; + config.peerToPeer.lan.isMdnsEnabled = true; + config.peerToPeer.lan.isMulticastEnabled = true; + }); + + // Set up authentication for server mode + await ditto.auth.setExpirationHandler( + async (dittoInstance, timeUntilExpiration) => { + console.log( + 'Authentication expiring soon, time until expiration:', + timeUntilExpiration, + ); + + if (dittoInstance.auth.loginSupported) { + const devProvider = Authenticator.DEVELOPMENT_PROVIDER; + const reLoginResult = await dittoInstance.auth.login( + this.token, + devProvider, + ); + if (reLoginResult.error) { + console.error('Re-authentication failed:', reLoginResult.error); + } else { + console.log( + 'Successfully re-authenticated with info:', + reLoginResult, + ); + } + } + }, + ); + + if (ditto.auth.loginSupported) { + const devProvider = Authenticator.DEVELOPMENT_PROVIDER; + const loginResult = await ditto.auth.login(this.token, devProvider); + if (loginResult.error) { + console.error('Login failed:', loginResult.error); + } else { + console.log('Successfully logged in with info:', loginResult); + } + } + + ditto.sync.start(); + + return ditto; + } + + startSync() { + this.ditto?.sync.start(); + } + + stopSync() { + this.ditto?.sync.stop(); + } +} diff --git a/javascript-tui/source/tasks-repository.js b/javascript-tui/source/tasks-repository.js new file mode 100644 index 000000000..b0715cfcd --- /dev/null +++ b/javascript-tui/source/tasks-repository.js @@ -0,0 +1,82 @@ +// Owns the tasks concern against a ready Ditto instance: registers the tasks +// subscription and observer, streams the observed task list to a callback, and +// exposes CRUD. Calls the Ditto API directly. +export class TasksRepository { + constructor(ditto, onTasksUpdated) { + this.ditto = ditto; + this.onTasksUpdated = onTasksUpdated; + this.subscription = null; + this.observer = null; + } + + // Registers the subscription (what syncs to this peer) and the observer + // (what is read from the local database). + start() { + // Register a subscription, which determines what data syncs to this peer + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + this.subscription = this.ditto.sync.registerSubscription( + 'SELECT * FROM tasks', + ); + + // Register observer, which runs against the local database on this peer + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + this.observer = this.ditto.store.registerObserver( + 'SELECT * FROM tasks WHERE NOT deleted', + (result) => { + const tasks = result.items.map((item) => item.value); + this.onTasksUpdated(tasks); + }, + ); + } + + // https://docs.ditto.live/sdk/latest/crud/create + async createTask(title) { + await this.ditto.store.execute('INSERT INTO tasks DOCUMENTS (:task)', { + task: { + title, + done: false, + deleted: false, + }, + }); + } + + // https://docs.ditto.live/sdk/latest/crud/update + async editTask(id, title) { + await this.ditto.store.execute( + 'UPDATE tasks SET title = :title WHERE _id = :id', + { + id, + title, + }, + ); + } + + // https://docs.ditto.live/sdk/latest/crud/update + async toggleTask(task) { + await this.ditto.store.execute( + 'UPDATE tasks SET done = :done WHERE _id = :id', + { + id: task._id, + done: !task.done, + }, + ); + } + + // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern + async deleteTask(task) { + await this.ditto.store.execute( + 'UPDATE tasks SET deleted = true WHERE _id = :id', + { + id: task._id, + }, + ); + } + + // Cancel the subscription and observer on teardown. + dispose() { + this.subscription?.cancel(); + this.observer?.cancel(); + this.subscription = null; + this.observer = null; + } +} diff --git a/javascript-tui/tests/integration_test.js b/javascript-tui/tests/integration_test.js index 052448b56..3069a93a3 100644 --- a/javascript-tui/tests/integration_test.js +++ b/javascript-tui/tests/integration_test.js @@ -1,12 +1,12 @@ #!/usr/bin/env node import React from 'react'; -import {render} from 'ink-testing-library'; +import { render } from 'ink-testing-library'; import dotenv from 'dotenv'; -import {Ditto, DittoConfig, Authenticator} from '@dittolive/ditto'; -import {temporaryDirectory} from 'tempy'; +import { Ditto, DittoConfig, Authenticator } from '@dittolive/ditto'; +import { temporaryDirectory } from 'tempy'; import App from '../dist/app.js'; -dotenv.config({path: '../.env'}); +dotenv.config({ path: '../.env' }); // Silence Ditto verbose logging for tests process.env.RUST_LOG = 'off'; @@ -15,83 +15,78 @@ const MAX_WAIT_ITERATIONS = 10; const POLL_INTERVAL_MS = 2000; async function createDittoInstance() { - const tempdir = temporaryDirectory(); - const appID = process.env.DITTO_APP_ID; - const token = process.env.DITTO_PLAYGROUND_TOKEN; - const authURL = process.env.DITTO_AUTH_URL; - const websocketURL = process.env.DITTO_WEBSOCKET_URL; - - const connectConfig = { - mode: 'server', - url: authURL, - }; - - const config = new DittoConfig(appID, connectConfig, tempdir); - const ditto = await Ditto.open(config); - - ditto.updateTransportConfig(config => { - config.connect.websocketURLs = [websocketURL]; - }); - - if (connectConfig.mode === 'server') { - await ditto.auth.setExpirationHandler( - async (dittoInstance, timeUntilExpiration) => { - if (dittoInstance.auth.loginSupported) { - const devProvider = Authenticator.DEVELOPMENT_PROVIDER; - await dittoInstance.auth.login(token, devProvider); - } - }, - ); - - if (ditto.auth.loginSupported) { - const devProvider = Authenticator.DEVELOPMENT_PROVIDER; - const loginResult = await ditto.auth.login(token, devProvider); - if (loginResult.error) { - throw new Error(`Login failed: ${loginResult.error}`); - } - } - } - - ditto.sync.start(); - return ditto; + const tempdir = temporaryDirectory(); + const databaseId = process.env.DITTO_DATABASE_ID; + const token = process.env.DITTO_DEVELOPMENT_TOKEN; + const serverURL = process.env.DITTO_SERVER_URL; + + const connectConfig = { + mode: 'server', + url: serverURL, + }; + + const config = new DittoConfig(databaseId, connectConfig, tempdir); + const ditto = await Ditto.open(config); + + if (connectConfig.mode === 'server') { + await ditto.auth.setExpirationHandler( + async (dittoInstance, timeUntilExpiration) => { + if (dittoInstance.auth.loginSupported) { + const devProvider = Authenticator.DEVELOPMENT_PROVIDER; + await dittoInstance.auth.login(token, devProvider); + } + }, + ); + + if (ditto.auth.loginSupported) { + const devProvider = Authenticator.DEVELOPMENT_PROVIDER; + const loginResult = await ditto.auth.login(token, devProvider); + if (loginResult.error) { + throw new Error(`Login failed: ${loginResult.error}`); + } + } + } + + ditto.sync.start(); + return ditto; } async function runIntegrationTest() { - let ditto; - try { - const expectedTitle = process.env.DITTO_CLOUD_TASK_TITLE; - - if (!expectedTitle || expectedTitle.trim() === '') { - throw new Error('Missing DITTO_CLOUD_TASK_TITLE environment variable'); - } - - ditto = await createDittoInstance(); - const {stdout} = render(React.createElement(App, {ditto})); - - for (let i = 1; i <= MAX_WAIT_ITERATIONS; i++) { - await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS)); - - const frame = stdout.lastFrame(); - const hasSyncActive = frame.includes('🟢 Sync Active'); - const hasTask = frame.includes(expectedTitle); - - if (hasSyncActive && hasTask) { - console.log('SUCCESS: Integration test passed!'); - await ditto.close(); - process.exit(0); - } - } - - console.error('FAILURE: Integration test conditions not met'); - await ditto.close(); - process.exit(1); - } catch (error) { - console.error('Integration test error:', error); - if (ditto) { - await ditto.close(); - } - process.exit(1); - } + let ditto; + try { + const expectedTitle = process.env.DITTO_CLOUD_TASK_TITLE; + + if (!expectedTitle || expectedTitle.trim() === '') { + throw new Error('Missing DITTO_CLOUD_TASK_TITLE environment variable'); + } + + ditto = await createDittoInstance(); + const { stdout } = render(React.createElement(App, { ditto })); + + for (let i = 1; i <= MAX_WAIT_ITERATIONS; i++) { + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); + + const frame = stdout.lastFrame(); + const hasSyncActive = frame.includes('🟢 Sync Active'); + const hasTask = frame.includes(expectedTitle); + + if (hasSyncActive && hasTask) { + console.log('SUCCESS: Integration test passed!'); + await ditto.close(); + process.exit(0); + } + } + + console.error('FAILURE: Integration test conditions not met'); + await ditto.close(); + process.exit(1); + } catch (error) { + console.error('Integration test error:', error); + if (ditto) { + await ditto.close(); + } + process.exit(1); + } } runIntegrationTest(); diff --git a/javascript-web/README.md b/javascript-web/README.md index 7054ac1d2..e6a96d3d1 100644 --- a/javascript-web/README.md +++ b/javascript-web/README.md @@ -4,12 +4,12 @@ This directory contains Ditto's quickstart app for in-browser web applications. This app uses Vite along with Typescript and React, and shows how to include the Ditto SDK in a client-side app running in the browser. -![JS Web Ditto Screenshot](../.github/assets/js-web-ditto-screenshot.png) +![JS Web Ditto Screenshot](../assets/js-web-ditto-screenshot.png) ## Documentation - [Javascript Install Guide](https://docs.ditto.live/sdk/latest/install-guides/js) -- [Javascript API Reference](https://software.ditto.live/js/Ditto/5.0.0/api-reference/) +- [Javascript API Reference](https://docs.ditto.live/sdk/latest/api-reference/js) - [Javascript Release Notes](https://docs.ditto.live/sdk/latest/release-notes/js) ## Prerequisites @@ -19,13 +19,13 @@ the Ditto SDK in a client-side app running in the browser. ## Getting Started To get started, you'll first need to create an app in the [Ditto Portal][0] -with the "Online Playground" authentication type. You'll need to find your -AppID and Playground Token in order to use this quickstart. +with the "Development" authentication type. You'll need to find your +Database ID and Development Token in order to use this quickstart. [0]: https://portal.ditto.live From the repo root, copy the `.env.sample` file to `.env`, and fill in the -fields with your AppID and Playground Token: +fields with your Database ID and Development Token: ``` cp .sample.env .env @@ -37,10 +37,10 @@ The `.env` file should look like this (with your fields filled in): #!/usr/bin/env bash # Copy this file from ".env.sample" to ".env", then fill in these values -# A Ditto AppID, Playground token, Auth URL, and Websocket URL can be obtained from https://portal.ditto.live -DITTO_APP_ID="" -DITTO_PLAYGROUND_TOKEN="" -DITTO_AUTH_URL="" +# A Ditto Database ID, Development token, and Server URL can be obtained from https://portal.ditto.live +DITTO_DATABASE_ID="" +DITTO_DEVELOPMENT_TOKEN="" +DITTO_SERVER_URL="" ``` Next, run the quickstart app with the following command: diff --git a/javascript-web/package-lock.json b/javascript-web/package-lock.json index 2983d9f13..b0f496ef0 100644 --- a/javascript-web/package-lock.json +++ b/javascript-web/package-lock.json @@ -8,7 +8,7 @@ "name": "ditto-quickstart-vite", "version": "0.0.0", "dependencies": { - "@dittolive/ditto": "5.0.0", + "@dittolive/ditto": "5.0.3", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -16,7 +16,7 @@ "@eslint/js": "^9.15.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react-swc": "^3.5.0", + "@vitejs/plugin-react-swc": "^4.3.2", "autoprefixer": "^10.4.20", "eslint": "^9.15.0", "eslint-config-prettier": "^10.1.5", @@ -29,7 +29,7 @@ "tailwindcss": "^3.4.16", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", - "vite": "^6.4.2" + "vite": "^8.0.10" } }, "node_modules/@alloc/quick-lru": { @@ -44,452 +44,58 @@ } }, "node_modules/@dittolive/ditto": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@dittolive/ditto/-/ditto-5.0.0.tgz", - "integrity": "sha512-Qv4bXhaXbCJlazFa2nLsx6LLeFohBuT0GupfXo7R+nalNGv5kzrJMuafbawydIuaP7ZgGn2e+39eiSny8iRSHg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@dittolive/ditto/-/ditto-5.0.3.tgz", + "integrity": "sha512-CDz3kRhSvBPsq95HUcIdA0CONFUXU5cEfZ0r60k++y6lmjgReJMLtimDfGoUp2YQufuYZpfmcXNp1x/OITBcMQ==", "license": "SEE LICENSE IN LICENSE.md", "engines": { "node": ">=14" + }, + "peerDependencies": { + "@expo/config-plugins": "*", + "expo-build-properties": "*" + }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + }, + "expo-build-properties": { + "optional": true + } } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -702,6 +308,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -734,6 +359,16 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "dev": true, @@ -745,29 +380,10 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -776,10 +392,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -788,12 +409,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -802,26 +426,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -830,26 +443,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -858,180 +460,135 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -1040,40 +597,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -1082,30 +650,28 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.15.18", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.46.tgz", + "integrity": "sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" + "@swc/types": "^0.1.27" }, "engines": { "node": ">=10" @@ -1115,16 +681,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.18", - "@swc/core-darwin-x64": "1.15.18", - "@swc/core-linux-arm-gnueabihf": "1.15.18", - "@swc/core-linux-arm64-gnu": "1.15.18", - "@swc/core-linux-arm64-musl": "1.15.18", - "@swc/core-linux-x64-gnu": "1.15.18", - "@swc/core-linux-x64-musl": "1.15.18", - "@swc/core-win32-arm64-msvc": "1.15.18", - "@swc/core-win32-ia32-msvc": "1.15.18", - "@swc/core-win32-x64-msvc": "1.15.18" + "@swc/core-darwin-arm64": "1.15.46", + "@swc/core-darwin-x64": "1.15.46", + "@swc/core-linux-arm-gnueabihf": "1.15.46", + "@swc/core-linux-arm64-gnu": "1.15.46", + "@swc/core-linux-arm64-musl": "1.15.46", + "@swc/core-linux-ppc64-gnu": "1.15.46", + "@swc/core-linux-s390x-gnu": "1.15.46", + "@swc/core-linux-x64-gnu": "1.15.46", + "@swc/core-linux-x64-musl": "1.15.46", + "@swc/core-win32-arm64-msvc": "1.15.46", + "@swc/core-win32-ia32-msvc": "1.15.46", + "@swc/core-win32-x64-msvc": "1.15.46" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1136,7 +704,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.18", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.46.tgz", + "integrity": "sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==", "cpu": [ "arm64" ], @@ -1151,9 +721,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", - "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.46.tgz", + "integrity": "sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==", "cpu": [ "x64" ], @@ -1168,9 +738,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", - "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.46.tgz", + "integrity": "sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==", "cpu": [ "arm" ], @@ -1185,13 +755,16 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", - "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.46.tgz", + "integrity": "sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1202,13 +775,56 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", - "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.46.tgz", + "integrity": "sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.46.tgz", + "integrity": "sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.46.tgz", + "integrity": "sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1219,13 +835,16 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", - "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.46.tgz", + "integrity": "sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1236,13 +855,16 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", - "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.46.tgz", + "integrity": "sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1253,9 +875,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", - "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.46.tgz", + "integrity": "sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==", "cpu": [ "arm64" ], @@ -1270,9 +892,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", - "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.46.tgz", + "integrity": "sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==", "cpu": [ "ia32" ], @@ -1287,9 +909,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", - "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.46.tgz", + "integrity": "sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==", "cpu": [ "x64" ], @@ -1305,17 +927,32 @@ }, "node_modules/@swc/counter": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.25", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "dev": true, @@ -1603,15 +1240,20 @@ } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.11.0", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.2.tgz", + "integrity": "sha512-MlSlmSAYbYwPjGa+CjOAqwij09iPYAQDHwx8osVkwoDamCxXlg+avpkC65Ysv+L/uqUWvRTsnJCKPjVhZih/sA==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.27", - "@swc/core": "^1.12.11" + "@rolldown/pluginutils": "^1.0.1", + "@swc/core": "^1.15.46" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7" + "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "node_modules/acorn": { @@ -1966,6 +1608,16 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "dev": true, @@ -1981,46 +1633,6 @@ "dev": true, "license": "ISC" }, - "node_modules/esbuild": { - "version": "0.25.12", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/escalade": { "version": "3.2.0", "dev": true, @@ -2567,6 +2179,279 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "dev": true, @@ -2659,7 +2544,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -2818,9 +2705,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "dev": true, "funding": [ { @@ -2838,7 +2725,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3104,47 +2991,38 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.59.0", + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/run-parallel": { @@ -3338,12 +3216,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -3406,6 +3286,14 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -3494,22 +3382,23 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.4.2", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3518,14 +3407,15 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -3534,13 +3424,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -3566,24 +3459,10 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { diff --git a/javascript-web/package.json b/javascript-web/package.json index 776208cd9..68f972829 100644 --- a/javascript-web/package.json +++ b/javascript-web/package.json @@ -11,7 +11,7 @@ "preview": "vite preview" }, "dependencies": { - "@dittolive/ditto": "5.0.0", + "@dittolive/ditto": "5.0.3", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -19,7 +19,7 @@ "@eslint/js": "^9.15.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react-swc": "^3.5.0", + "@vitejs/plugin-react-swc": "^4.3.2", "autoprefixer": "^10.4.20", "eslint": "^9.15.0", "eslint-config-prettier": "^10.1.5", @@ -32,7 +32,7 @@ "tailwindcss": "^3.4.16", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", - "vite": "^6.4.2" + "vite": "^8.0.10" }, "overrides": { "flatted": "3.4.2" diff --git a/javascript-web/src/App.tsx b/javascript-web/src/App.tsx index 902d8f708..7ff83f8c0 100644 --- a/javascript-web/src/App.tsx +++ b/javascript-web/src/App.tsx @@ -1,15 +1,8 @@ -import { - Authenticator, - Ditto, - DittoConfig, - StoreObserver, - SyncSubscription, - init, -} from '@dittolive/ditto'; import DittoInfo from './components/DittoInfo'; import ErrorMessage from './components/ErrorMessage'; -import { useCallback, useEffect, useRef, useState } from 'react'; import TaskList from './components/TaskList'; +import { useDitto } from './hooks/useDitto'; +import { useTasks } from './hooks/useTasks'; export type Task = { _id: string; @@ -19,213 +12,17 @@ export type Task = { }; const App = () => { - const [error, setError] = useState(null); - const ditto = useRef(null); - const tasksSubscription = useRef(null); - const tasksObserver = useRef(null); - const isInitializing = useRef(false); // Persist across renders - - const [syncActive, setSyncActive] = useState(true); - const [isInitialized, setIsInitialized] = useState(false); - - const [tasks, setTasks] = useState(null); - - useEffect(() => { - let isMounted = true; // Prevent state updates after unmount - - const initializeDitto = async () => { - // Skip if Ditto already exists (handles StrictMode double-mount) - if (ditto.current) { - console.log('Skipping init - Ditto already exists'); - return; - } - // Prevent concurrent initializations - if (isInitializing.current) { - console.log('Skipping init - initialization in progress'); - return; - } - isInitializing.current = true; - console.log('Starting Ditto initialization...'); - try { - // Step 1: Initialize WASM (MUST be first) - await init(); - - // Step 2: Create config (AFTER init) - const config = new DittoConfig(import.meta.env.DITTO_APP_ID, { - mode: 'server', - url: import.meta.env.DITTO_AUTH_URL, - }); - - // Step 3: Open Ditto instance - ditto.current = await Ditto.open(config); - - // Step 4: Set up authentication expiration handler (required for server connections) - await ditto.current.auth.setExpirationHandler(async (dittoInstance) => { - // Authenticate when token is expiring. Any errors will be logged in the Ditto logger. - const loginResult = await dittoInstance.auth.login( - import.meta.env.DITTO_PLAYGROUND_TOKEN, - Authenticator.DEVELOPMENT_PROVIDER, - ); - if (loginResult.error) { - console.error('āŒ Re-authentication failed:', loginResult.error); - } else { - console.log( - 'āœ… Successfully re-authenticated with info:', - loginResult, - ); - } - }); - - // Step 5: Configure transport - ditto.current.updateTransportConfig((config) => { - config.connect.websocketURLs = [import.meta.env.DITTO_WEBSOCKET_URL]; - }); - - // Step 6: Start sync - ditto.current.sync.start(); - - // Step 7: Register subscription - // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions - tasksSubscription.current = ditto.current.sync.registerSubscription( - 'SELECT * FROM tasks', - ); - - // Step 8: Register observer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - tasksObserver.current = ditto.current.store.registerObserver( - 'SELECT * FROM tasks WHERE deleted=false ORDER BY title ASC', - (results) => { - console.log('Observer', results); - if (isMounted) { - const tasks = results.items.map((item) => item.value); - setTasks(tasks); - } - }, - ); - - // Step 9: Mark as initialized - if (isMounted) { - setIsInitialized(true); - } - isInitializing.current = false; - console.log('āœ… Ditto initialized successfully'); - } catch (e) { - console.error('āŒ Ditto initialization failed:', e); - isInitializing.current = false; - if (isMounted) { - setError(e as Error); - setIsInitialized(false); - } - } - }; - - initializeDitto(); - - // Cleanup function (returned from useEffect, not inside IIFE) - return () => { - // In development with StrictMode, skip ALL cleanup to avoid lock file issues - // and keep observers active. This handles StrictMode's intentional unmount/remount. - if (import.meta.env.DEV) { - console.log('Dev mode - skipping ALL cleanup to handle StrictMode'); - return; - } - - // In production, properly clean up everything - console.log('Production cleanup - closing Ditto'); - isMounted = false; - tasksObserver.current?.cancel(); - tasksSubscription.current?.cancel(); - - if (ditto.current) { - try { - ditto.current.close(); - } catch (e) { - console.error('Error closing Ditto:', e); - } - ditto.current = null; - } - }; - }, []); // Empty deps - run once on mount - - const toggleSync = useCallback(() => { - setSyncActive((prev) => { - if (prev) { - ditto.current?.sync.stop(); - } else { - ditto.current?.sync.start(); - } - return !prev; - }); - }, []); - - // https://docs.ditto.live/sdk/latest/crud/create - const createTask = useCallback(async (title: string) => { - try { - await ditto.current?.store.execute( - 'INSERT INTO tasks DOCUMENTS (:task)', - { - task: { - title, - done: false, - deleted: false, - }, - }, - ); - } catch (error) { - console.error('Failed to create task:', error); - } - }, []); - - // https://docs.ditto.live/sdk/latest/crud/update - const editTask = useCallback(async (id: string, title: string) => { - try { - await ditto.current?.store.execute( - 'UPDATE tasks SET title=:title WHERE _id=:id', - { - id, - title, - }, - ); - } catch (error) { - console.error('Failed to edit task:', error); - } - }, []); - - const toggleTask = useCallback(async (task: Task) => { - try { - await ditto.current?.store.execute( - 'UPDATE tasks SET done=:done WHERE _id=:id', - { - id: task._id, - done: !task.done, - }, - ); - } catch (error) { - console.error('Failed to toggle task:', error); - } - }, []); - - // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern - const deleteTask = useCallback(async (task: Task) => { - try { - await ditto.current?.store.execute( - 'UPDATE tasks SET deleted=true WHERE _id=:id', - { - id: task._id, - }, - ); - } catch (error) { - console.error('Failed to delete task:', error); - } - }, []); + const { ditto, syncActive, toggleSync, isInitialized, error } = useDitto(); + const { tasks, createTask, editTask, toggleTask, deleteTask } = + useTasks(ditto); return (
    {error && } void; @@ -9,7 +9,7 @@ type Props = { }; const DittoInfo: React.FC = ({ - appId, + databaseId, token, syncEnabled, onToggleSync, @@ -21,7 +21,7 @@ const DittoInfo: React.FC = ({ Ditto Tasks
    -

    App ID: {appId}

    +

    Database ID: {databaseId}

    Token: {token}

    { + const [error, setError] = useState(null); + const ditto = useRef(null); + const isInitializing = useRef(false); // Persist across renders + + const [syncActive, setSyncActive] = useState(true); + const [isInitialized, setIsInitialized] = useState(false); + + // Exposed to consumers as state so dependent hooks re-run once Ditto is ready. + const [dittoInstance, setDittoInstance] = useState(null); + + useEffect(() => { + const initializeDitto = async () => { + // Skip if Ditto already exists (handles StrictMode double-mount) + if (ditto.current) { + console.log('Skipping init - Ditto already exists'); + return; + } + // Prevent concurrent initializations + if (isInitializing.current) { + console.log('Skipping init - initialization in progress'); + return; + } + isInitializing.current = true; + console.log('Starting Ditto initialization...'); + try { + // Step 1: Initialize WASM (MUST be first) + await init(); + + // Step 2: Create config (AFTER init) + const config = new DittoConfig(import.meta.env.DITTO_DATABASE_ID, { + mode: 'server', + url: import.meta.env.DITTO_SERVER_URL, + }); + + // Step 3: Open Ditto instance + ditto.current = await Ditto.open(config); + + // Step 4: Set up authentication expiration handler (required for server connections) + await ditto.current.auth.setExpirationHandler(async (dittoInstance) => { + // Authenticate when token is expiring. Any errors will be logged in the Ditto logger. + const loginResult = await dittoInstance.auth.login( + import.meta.env.DITTO_DEVELOPMENT_TOKEN, + Authenticator.DEVELOPMENT_PROVIDER, + ); + if (loginResult.error) { + console.error('āŒ Re-authentication failed:', loginResult.error); + } else { + console.log( + 'āœ… Successfully re-authenticated with info:', + loginResult, + ); + } + }); + + // Step 5: Start sync + ditto.current.sync.start(); + + // Step 6: Mark as initialized and expose the ready instance + setIsInitialized(true); + setDittoInstance(ditto.current); + isInitializing.current = false; + console.log('āœ… Ditto initialized successfully'); + } catch (e) { + console.error('āŒ Ditto initialization failed:', e); + isInitializing.current = false; + setError(e as Error); + setIsInitialized(false); + } + }; + + initializeDitto(); + + // Cleanup function (returned from useEffect, not inside IIFE) + return () => { + // In development with StrictMode, skip ALL cleanup to avoid lock file issues + // and keep observers active. This handles StrictMode's intentional unmount/remount. + if (import.meta.env.DEV) { + console.log('Dev mode - skipping ALL cleanup to handle StrictMode'); + return; + } + + // In production, properly clean up everything + console.log('Production cleanup - closing Ditto'); + + if (ditto.current) { + try { + ditto.current.close(); + } catch (e) { + console.error('Error closing Ditto:', e); + } + ditto.current = null; + } + }; + }, []); // Empty deps - run once on mount + + const toggleSync = useCallback(() => { + setSyncActive((prev) => { + if (prev) { + ditto.current?.sync.stop(); + } else { + ditto.current?.sync.start(); + } + return !prev; + }); + }, []); + + return { + ditto: dittoInstance, + syncActive, + toggleSync, + isInitialized, + error, + }; +}; diff --git a/javascript-web/src/hooks/useTasks.ts b/javascript-web/src/hooks/useTasks.ts new file mode 100644 index 000000000..cf790c9e7 --- /dev/null +++ b/javascript-web/src/hooks/useTasks.ts @@ -0,0 +1,130 @@ +import { Ditto, StoreObserver, SyncSubscription } from '@dittolive/ditto'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { Task } from '../App'; + +/** + * TasksRepository hook: registers the tasks subscription and observer, holds + * the tasks state, and exposes CRUD. It calls the real Ditto API directly and + * takes the ready `ditto` instance from `useDitto`. + */ +export const useTasks = (ditto: Ditto | null) => { + const tasksSubscription = useRef(null); + const tasksObserver = useRef(null); + + // Mirror the latest instance into a ref so the CRUD callbacks stay stable + // (empty deps) while always reading the current Ditto instance. + const dittoRef = useRef(ditto); + dittoRef.current = ditto; + + const [tasks, setTasks] = useState(null); + + useEffect(() => { + if (!ditto) { + return; + } + + let isMounted = true; // Prevent state updates after unmount + + // Register subscription + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + tasksSubscription.current = ditto.sync.registerSubscription( + 'SELECT * FROM tasks', + ); + + // Register observer + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + tasksObserver.current = ditto.store.registerObserver( + 'SELECT * FROM tasks WHERE NOT deleted', + (results) => { + console.log('Observer', results); + if (isMounted) { + const tasks = results.items.map((item) => item.value); + setTasks(tasks); + } + }, + ); + + return () => { + // In development with StrictMode, skip ALL cleanup to avoid lock file issues + // and keep observers active. This handles StrictMode's intentional unmount/remount. + if (import.meta.env.DEV) { + console.log('Dev mode - skipping ALL cleanup to handle StrictMode'); + return; + } + + // In production, properly clean up the subscription and observer + isMounted = false; + tasksObserver.current?.cancel(); + tasksSubscription.current?.cancel(); + }; + }, [ditto]); + + // https://docs.ditto.live/sdk/latest/crud/create + const createTask = useCallback(async (title: string) => { + try { + await dittoRef.current?.store.execute( + 'INSERT INTO tasks DOCUMENTS (:task)', + { + task: { + title, + done: false, + deleted: false, + }, + }, + ); + } catch (error) { + console.error('Failed to create task:', error); + } + }, []); + + // https://docs.ditto.live/sdk/latest/crud/update + const editTask = useCallback(async (id: string, title: string) => { + try { + await dittoRef.current?.store.execute( + 'UPDATE tasks SET title = :title WHERE _id = :id', + { + id, + title, + }, + ); + } catch (error) { + console.error('Failed to edit task:', error); + } + }, []); + + const toggleTask = useCallback(async (task: Task) => { + try { + await dittoRef.current?.store.execute( + 'UPDATE tasks SET done = :done WHERE _id = :id', + { + id: task._id, + done: !task.done, + }, + ); + } catch (error) { + console.error('Failed to toggle task:', error); + } + }, []); + + // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern + const deleteTask = useCallback(async (task: Task) => { + try { + await dittoRef.current?.store.execute( + 'UPDATE tasks SET deleted = true WHERE _id = :id', + { + id: task._id, + }, + ); + } catch (error) { + console.error('Failed to delete task:', error); + } + }, []); + + return { + tasks, + createTask, + editTask, + toggleTask, + deleteTask, + }; +}; diff --git a/kotlin-multiplatform/.run/composeApp [desktop].run.xml b/kotlin-multiplatform/.run/composeApp [desktop].run.xml index d3c4831f5..90190d272 100644 --- a/kotlin-multiplatform/.run/composeApp [desktop].run.xml +++ b/kotlin-multiplatform/.run/composeApp [desktop].run.xml @@ -4,7 +4,7 @@ @@ -21,4 +21,4 @@ false - \ No newline at end of file + diff --git a/kotlin-multiplatform/README.md b/kotlin-multiplatform/README.md index dac8044c7..b4b046b29 100644 --- a/kotlin-multiplatform/README.md +++ b/kotlin-multiplatform/README.md @@ -1,24 +1,127 @@ # Ditto Kotlin Multiplatform Quickstart App šŸš€ +This quickstart implements the same Tasks application for Android, iOS, and +Compose Desktop using shared Kotlin and Compose Multiplatform code. + ## Prerequisites -For more information, see - [Kotlin Multiplatform Install Guide](https://docs.ditto.live/sdk/latest/install-guides/kotlin/multiplatform) +- A Ditto application created in the [Ditto Portal](https://portal.ditto.live/) +- JDK 17 +- The tools required by the platform you want to run: + - Android Studio and the Android SDK for Android + - Xcode on macOS for iOS + - A supported desktop host: macOS Apple Silicon, Windows x64, or Linux + +The Android SDK is not required when building only the desktop or iOS target. + +## Configure Ditto + +The app reads its Ditto configuration from `quickstart/.env`, the parent +directory of this project. From `quickstart/kotlin-multiplatform`, create it +from the tracked sample: + +macOS or Linux: + +```shell +cp ../.env.sample ../.env +``` + +Windows PowerShell: + +```powershell +Copy-Item ..\.env.sample ..\.env +``` + +Set all three values using the credentials and server URL from the Ditto +Portal: + +```dotenv +DITTO_DATABASE_ID="your-database-id" +DITTO_DEVELOPMENT_TOKEN="your-development-token" +DITTO_SERVER_URL="https://your-server-url" +``` + +The `.env` file is ignored by Git. These values are compiled into generated +quickstart source code, so do not commit the file or use a development token in +a production application. + +## Build configuration + +This standalone quickstart supports one optional Gradle project property: + +| Property | Default | Effect | +| --- | --- | --- | +| `ditto.skipAndroidBuilds` | `false` | When `true`, does not apply the Android plugin or configure Android targets, source sets, tests, and dependencies. Use it for desktop or iOS builds on a machine without an Android SDK. | + +Pass Gradle project properties with `-P`. Do not use +`ditto.skipAndroidBuilds=true` when building or running Android. + +## Run the app + +Run Gradle commands from `quickstart/kotlin-multiplatform`. + +### Compose Desktop + +The desktop build does not need an Android SDK when the Android target is +disabled. + +macOS or Linux: + +```shell +./gradlew -Pditto.skipAndroidBuilds=true :composeApp:run +``` + +Windows PowerShell: + +```powershell +.\gradlew.bat "-Pditto.skipAndroidBuilds=true" :composeApp:run +``` + +The checked-in `composeApp [desktop]` IDE run configuration already supplies +this property. + +To create an installable package for the current desktop operating system, +replace `:composeApp:run` with +`:composeApp:packageDistributionForCurrentOS`. + +### Android + +Open this directory in Android Studio, select the `composeApp` run +configuration, and run it on a device or emulator. To build from the command +line: + +```shell +./gradlew :composeApp:assembleDebug +``` + +On Windows, use `.\gradlew.bat` in place of `./gradlew`. + +### iOS + +Open the Xcode project and run the `iosApp` scheme: + +```shell +open iosApp/iosApp.xcodeproj +``` + +The Xcode build phase disables the Android target, so an Android SDK is not +required. To build the simulator framework directly with Gradle: + +```shell +./gradlew -Pditto.skipAndroidBuilds=true :composeApp:linkDebugFrameworkIosSimulatorArm64 +``` -## Getting Started +## Troubleshooting -1. Create an application at . Make note of the app ID and online playground token. -2. Copy the `.env.template` file at the top level of the `quickstart` repo to `.env` and add your app ID and online playground token. -3. Synchronize your project with the Gradle file by clicking Build > Sync Project with Gradle Files. -4. Running app in the desired platform: - 1. Android: - On Android Studio, run the `composeApp` application - 2. Compose Desktop - Execute `./gradlew :composeApp:run` - 3. iOS - Execute `open iosApp/iosApp.xcodeproj` or open `iosApp/iosApp.xcodeproj` in Xcode - Run the application in Xcode +- If a desktop build asks for an Android SDK, make sure + `-Pditto.skipAndroidBuilds=true` is present. +- If `generateSecretProperties` reports that `.env` is missing, confirm the + file is at `quickstart/.env`, not inside `kotlin-multiplatform`. +- Rerun the Gradle task after changing `.env` so the generated configuration is + refreshed. -## Additional Resources +## Additional resources -- [Kotlin Multiplatform Roadmap and Support Policy](https://docs.ditto.live/sdk/latest/install-guides/kotlin/multiplatform-roadmap) -- [API Reference](https://software.ditto.live/java/ditto-java/5.0.0-preview.3/api-reference/) +- [Kotlin install guide](https://docs.ditto.live/sdk/latest/install-guides/kotlin) +- [Kotlin compatibility](https://docs.ditto.live/sdk/latest/compatibility/kotlin) +- [Kotlin release notes](https://docs.ditto.live/sdk/latest/release-notes/kotlin) diff --git a/kotlin-multiplatform/composeApp/build.gradle.kts b/kotlin-multiplatform/composeApp/build.gradle.kts index 1698953c4..6a2905b20 100644 --- a/kotlin-multiplatform/composeApp/build.gradle.kts +++ b/kotlin-multiplatform/composeApp/build.gradle.kts @@ -1,11 +1,11 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.compose.ExperimentalComposeLibrary plugins { alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidApplication) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.detekt) @@ -13,13 +13,21 @@ plugins { id("quickstart-conventions") } +val skipAndroidBuilds = findProperty("ditto.skipAndroidBuilds")?.toString().toBoolean() + +if (!skipAndroidBuilds) { + apply(plugin = libs.plugins.androidApplication.get().pluginId) +} + kotlin { jvmToolchain(libs.versions.java.get().toInt()) - androidTarget { - @OptIn(ExperimentalKotlinGradlePluginApi::class) - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + if (!skipAndroidBuilds) { + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } } } @@ -39,13 +47,15 @@ kotlin { sourceSets { val desktopMain by getting - androidMain.dependencies { - implementation(compose.preview) - implementation(libs.androidx.activity.compose) - implementation(libs.koin.android) + if (!skipAndroidBuilds) { + androidMain.dependencies { + implementation(compose.preview) + implementation(libs.androidx.activity.compose) + implementation(libs.koin.android) + } } commonMain.dependencies { - implementation("com.ditto:ditto-kotlin:5.0.0-preview.3") + implementation("com.ditto:ditto-kotlin:5.0.3") implementation(compose.runtime) implementation(compose.foundation) @@ -68,14 +78,16 @@ kotlin { implementation(libs.kotlin.test) } - val androidInstrumentedTest by getting { - dependencies { - implementation(libs.androidx.test.junit) - implementation(libs.androidx.test.runner) - implementation("androidx.test.uiautomator:uiautomator:2.3.0") - implementation("androidx.tracing:tracing:1.1.0") - @OptIn(ExperimentalComposeLibrary::class) - implementation(compose.uiTest) + if (!skipAndroidBuilds) { + val androidInstrumentedTest by getting { + dependencies { + implementation(libs.androidx.test.junit) + implementation(libs.androidx.test.runner) + implementation("androidx.test.uiautomator:uiautomator:2.3.0") + implementation("androidx.tracing:tracing:1.1.0") + @OptIn(ExperimentalComposeLibrary::class) + implementation(compose.uiTest) + } } } desktopMain.dependencies { @@ -83,26 +95,26 @@ kotlin { implementation(libs.kotlinx.coroutines.swing) // This will include binaries for all the supported platforms and architectures - implementation("com.ditto:ditto-binaries:5.0.0-preview.3") + implementation("com.ditto:ditto-binaries:5.0.3") // To reduce your module artifact's size, consider including just the necessary platforms and architectures /* // macOS Apple Silicon - implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { + implementation("com.ditto:ditto-binaries:5.0.3") { capabilities { requireCapability("com.ditto:ditto-binaries-macos-arm64") } } // Windows x86_64 - implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { + implementation("com.ditto:ditto-binaries:5.0.3") { capabilities { requireCapability("com.ditto:ditto-binaries-windows-x64") } } // Linux x86_64 - implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { + implementation("com.ditto:ditto-binaries:5.0.3") { capabilities { requireCapability("com.ditto:ditto-binaries-linux-x64") } @@ -112,46 +124,61 @@ kotlin { } } -android { - namespace = "com.ditto.quickstart" - compileSdk = libs.versions.android.compileSdk.get().toInt() +if (!skipAndroidBuilds) { + configure { + namespace = "com.ditto.quickstart" + compileSdk = libs.versions.android.compileSdk.get().toInt() - // Force consistent androidx.tracing version to resolve test dependency conflicts - configurations.all { - resolutionStrategy { - force("androidx.tracing:tracing:1.1.0") + // Force consistent androidx.tracing version to resolve test dependency conflicts + configurations.all { + resolutionStrategy { + force("androidx.tracing:tracing:1.1.0") + } } - } - defaultConfig { - applicationId = "com.ditto.quickstart" - minSdk = libs.versions.android.minSdk.get().toInt() - targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = 1 - versionName = "1.0" + defaultConfig { + applicationId = "com.ditto.quickstart" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - // Pass environment variables to instrumented tests - testInstrumentationRunnerArguments["DITTO_CLOUD_TASK_TITLE"] = System.getenv("DITTO_CLOUD_TASK_TITLE") ?: "" - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" + // Pass environment variables to instrumented tests + testInstrumentationRunnerArguments["DITTO_CLOUD_TASK_TITLE"] = + System.getenv("DITTO_CLOUD_TASK_TITLE") ?: "" } - } - buildTypes { - getByName("release") { - isMinifyEnabled = false + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + + // https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html + val javaVersion = JavaVersion.valueOf("VERSION_" + libs.versions.java.get()) + + compileOptions { + targetCompatibility = javaVersion + sourceCompatibility = javaVersion } } - // https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html - val javaVersion = JavaVersion.valueOf("VERSION_" + libs.versions.java.get()) + dependencies { + add("implementation", libs.androidx.material3.android) + add("debugImplementation", compose.uiTooling) - compileOptions { - targetCompatibility = javaVersion - sourceCompatibility = javaVersion + add("androidTestImplementation", libs.androidx.test.junit) + add("androidTestImplementation", libs.androidx.test.runner) + add("androidTestImplementation", libs.androidx.test.rules) + add("androidTestImplementation", libs.androidx.ui.test.junit4) + @OptIn(ExperimentalComposeLibrary::class) + add("androidTestImplementation", compose.uiTest) } } @@ -164,18 +191,6 @@ detekt { parallel = true } -dependencies { - implementation(libs.androidx.material3.android) - debugImplementation(compose.uiTooling) - - androidTestImplementation(libs.androidx.test.junit) - androidTestImplementation(libs.androidx.test.runner) - androidTestImplementation(libs.androidx.test.rules) - androidTestImplementation(libs.androidx.ui.test.junit4) - @OptIn(ExperimentalComposeLibrary::class) - androidTestImplementation(compose.uiTest) -} - compose.desktop { application { mainClass = "com.ditto.quickstart.MainKt" diff --git a/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt b/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt index 2a30cd607..86320d31a 100644 --- a/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt +++ b/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt @@ -3,10 +3,8 @@ package com.ditto.quickstart.ditto import com.ditto.kotlin.Ditto import com.ditto.kotlin.DittoConfig import com.ditto.kotlin.DittoFactory -import com.ditto.quickstart.App actual fun createDitto(config: DittoConfig): Ditto = DittoFactory.create( - context = App.instance, config = config, ) diff --git a/kotlin-multiplatform/composeApp/src/androidMain/res/xml/network_security_config.xml b/kotlin-multiplatform/composeApp/src/androidMain/res/xml/network_security_config.xml index 4f157a7ab..782b46b87 100644 --- a/kotlin-multiplatform/composeApp/src/androidMain/res/xml/network_security_config.xml +++ b/kotlin-multiplatform/composeApp/src/androidMain/res/xml/network_security_config.xml @@ -10,7 +10,7 @@ - + browserstack.com percy.io diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/DittoCredentials.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/DittoCredentials.kt index 32398477b..6a44f4a68 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/DittoCredentials.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/DittoCredentials.kt @@ -1,6 +1,6 @@ package com.ditto.quickstart.data data class DittoCredentials( - val appId: String, + val databaseId: String, val appToken: String ) diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/dto/AddTaskDto.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/dto/AddTaskDto.kt index 9917618b0..079cbc062 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/dto/AddTaskDto.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/dto/AddTaskDto.kt @@ -1,18 +1,9 @@ package com.ditto.quickstart.data.dto -import com.ditto.kotlin.serialization.DittoCborSerializable -import com.ditto.kotlin.serialization.DittoCborSerializable.Utf8String - data class AddTaskDto( val title: String, val done: Boolean, val deleted: Boolean, ) -fun AddTaskDto.toDittoDictionary() = DittoCborSerializable.Dictionary(mapOf( - Utf8String("title") to Utf8String(title), - Utf8String("done") to DittoCborSerializable.BooleanValue(done), - Utf8String("deleted") to DittoCborSerializable.BooleanValue(deleted), -)) - diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/DittoTaskRepository.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/DittoTaskRepository.kt deleted file mode 100644 index 464fce7d1..000000000 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/DittoTaskRepository.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.ditto.quickstart.data.repository - -import com.ditto.kotlin.DittoQueryResultItem -import com.ditto.kotlin.DittoSyncSubscription -import com.ditto.kotlin.serialization.DittoCborSerializable -import com.ditto.kotlin.serialization.DittoCborSerializable.Utf8String -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import com.ditto.quickstart.data.Task -import com.ditto.quickstart.data.dto.AddTaskDto -import com.ditto.quickstart.data.dto.toDittoDictionary -import com.ditto.quickstart.data.dto.UpdateTaskDoneDto -import com.ditto.quickstart.data.dto.UpdateTaskTitleDto -import com.ditto.quickstart.ditto.DittoManager - -private const val SUBSCRIPTION_QUERY_SELECT_TASKS = """ -SELECT * FROM tasks WHERE NOT deleted -""" - -private const val QUERY_SELECT_TASKS = """ -SELECT * FROM tasks WHERE NOT deleted ORDER BY title ASC -""" - -private const val QUERY_SELECT_TASK = """ -SELECT * FROM tasks WHERE NOT deleted AND _id = :taskId LIMIT 1 -""" - -private const val QUERY_INSERT_TASK = """ -INSERT INTO tasks DOCUMENTS (:task) -""" - -private const val QUERY_UPDATE_TASK_TITLE = """ -UPDATE tasks SET title = :title WHERE _id = :taskId -""" - -private const val QUERY_UPDATE_TASK_DONE = """ -UPDATE tasks SET done = :done WHERE _id = :taskId -""" - -private const val QUERY_UPDATE_TASK_DELETED = """ -UPDATE tasks SET deleted = :deleted WHERE _id = :taskId -""" - -class DittoTaskRepository( - private val dittoManager: DittoManager, -) : TaskRepository { - private var syncSubscription: DittoSyncSubscription? = null - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - private val tasksMutableStateFlow = MutableStateFlow(emptyList()) - override val tasksStateFlow: StateFlow> = tasksMutableStateFlow.asStateFlow() - .onStart { - registerSubscription() - registerObserver() - } - .stateIn( - scope = scope, - started = SharingStarted.Companion.WhileSubscribed(5000L), - initialValue = emptyList() - ) - - override suspend fun getTask(taskId: String): Task? { - return dittoManager.executeDql( - query = QUERY_SELECT_TASK, - parameters = DittoCborSerializable.Dictionary( - mapOf(Utf8String("taskId") to Utf8String(taskId)) - ) - )?.items?.firstOrNull()?.toTask() - } - - override suspend fun addTask(addTaskDto: AddTaskDto) { - dittoManager.executeDql( - query = QUERY_INSERT_TASK, - parameters = DittoCborSerializable.Dictionary( - mapOf(Utf8String("task") to addTaskDto.toDittoDictionary()) - ) - ) - } - - override suspend fun updateTaskTitle(updateTaskTitleDto: UpdateTaskTitleDto) { - dittoManager.executeDql( - query = QUERY_UPDATE_TASK_TITLE, - parameters = DittoCborSerializable.Dictionary(mapOf( - Utf8String("title") to Utf8String(updateTaskTitleDto.title), - Utf8String("taskId") to Utf8String(updateTaskTitleDto.id), - )) - ) - } - - override suspend fun updateTaskDone(updateTaskDoneDto: UpdateTaskDoneDto) { - dittoManager.executeDql( - query = QUERY_UPDATE_TASK_DONE, - parameters = DittoCborSerializable.Dictionary(mapOf( - Utf8String("taskId") to Utf8String(updateTaskDoneDto.id), - Utf8String("done") to DittoCborSerializable.BooleanValue(updateTaskDoneDto.done) - )) - ) - } - - override suspend fun removeTask(taskId: String) { - dittoManager.executeDql( - query = QUERY_UPDATE_TASK_DELETED, - parameters = DittoCborSerializable.Dictionary(mapOf( - Utf8String("deleted") to DittoCborSerializable.BooleanValue(true), - Utf8String("taskId") to Utf8String(taskId), - )) - ) - } - - override fun onCleared() { - syncSubscription?.close() - syncSubscription = null - - scope.cancel() - } - - private fun DittoQueryResultItem.toTask(): Task = Task( - id = this.value["_id"].string, - title = this.value["title"].string, - done = this.value["done"].boolean, - deleted = this.value["deleted"].boolean, - ) - - private suspend fun registerSubscription() { - syncSubscription = dittoManager.registerSubscription(SUBSCRIPTION_QUERY_SELECT_TASKS) - } - - private suspend fun registerObserver() { - val observer = dittoManager.registerObserver(QUERY_SELECT_TASKS) - scope.launch { - observer - .map { result -> result.items.map { item -> item.toTask() } } - .collect { tasks -> - // Use database ordering (ORDER BY title ASC) - no client-side sorting needed - tasksMutableStateFlow.value = tasks - } - } - } -} diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/TaskRepository.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/TaskRepository.kt index 67d15dbf2..136357ec5 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/TaskRepository.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/TaskRepository.kt @@ -1,13 +1,15 @@ package com.ditto.quickstart.data.repository -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow import com.ditto.quickstart.data.Task import com.ditto.quickstart.data.dto.AddTaskDto import com.ditto.quickstart.data.dto.UpdateTaskDoneDto import com.ditto.quickstart.data.dto.UpdateTaskTitleDto interface TaskRepository { - val tasksStateFlow: StateFlow> + // Emits the visible task list (soft-deleted tasks excluded) and re-emits whenever + // the local store changes. + fun observeTasks(): Flow> suspend fun getTask(taskId: String): Task? suspend fun addTask(addTaskDto: AddTaskDto) diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/TasksRepository.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/TasksRepository.kt new file mode 100644 index 000000000..e27a4a8cd --- /dev/null +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/TasksRepository.kt @@ -0,0 +1,130 @@ +package com.ditto.quickstart.data.repository + +import com.ditto.kotlin.DittoQueryResultItem +import com.ditto.kotlin.DittoSyncSubscription +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import com.ditto.quickstart.data.Task +import com.ditto.quickstart.data.dto.AddTaskDto +import com.ditto.quickstart.data.dto.UpdateTaskDoneDto +import com.ditto.quickstart.data.dto.UpdateTaskTitleDto +import com.ditto.quickstart.ditto.DittoManager + +// The subscription controls what syncs to this device and is written to the +// local database; the observer (QUERY_SELECT_TASKS below) reacts to changes in +// that local database, hiding soft-deleted tasks from the displayed list. +private const val SUBSCRIPTION_QUERY_SELECT_TASKS = """ +SELECT * FROM tasks +""" + +private const val QUERY_SELECT_TASKS = """ +SELECT * FROM tasks WHERE NOT deleted +""" + +private const val QUERY_SELECT_TASK = """ +SELECT * FROM tasks WHERE _id = :id +""" + +private const val QUERY_INSERT_TASK = """ +INSERT INTO tasks DOCUMENTS (:task) +""" + +private const val QUERY_UPDATE_TASK_TITLE = """ +UPDATE tasks SET title = :title WHERE _id = :id +""" + +private const val QUERY_UPDATE_TASK_DONE = """ +UPDATE tasks SET done = :done WHERE _id = :id +""" + +private const val QUERY_UPDATE_TASK_DELETED = """ +UPDATE tasks SET deleted = true WHERE _id = :id +""" + +class TasksRepository( + private val dittoManager: DittoManager, +) : TaskRepository { + private var syncSubscription: DittoSyncSubscription? = null + + // Emits the visible task list (soft-deleted tasks excluded) and re-emits on every + // change to the local store. Registers the sync subscription when collection starts. + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + override fun observeTasks(): Flow> = flow { + registerSubscription() + val ditto = dittoManager.getDitto() ?: return@flow + emitAll( + ditto.store.observe(QUERY_SELECT_TASKS) { result -> + result.items.map { item -> item.toTask() } + } + ) + } + + override suspend fun getTask(taskId: String): Task? = + dittoManager.getDitto()?.store?.execute( + QUERY_SELECT_TASK, + mapOf("id" to taskId) + ) { result -> + result.items.firstOrNull()?.toTask() + } + + override suspend fun addTask(addTaskDto: AddTaskDto) { + dittoManager.getDitto()?.store?.execute( + QUERY_INSERT_TASK, + mapOf( + "task" to mapOf( + "title" to addTaskDto.title, + "done" to addTaskDto.done, + "deleted" to addTaskDto.deleted, + ) + ) + ) + } + + override suspend fun updateTaskTitle(updateTaskTitleDto: UpdateTaskTitleDto) { + dittoManager.getDitto()?.store?.execute( + QUERY_UPDATE_TASK_TITLE, + mapOf( + "title" to updateTaskTitleDto.title, + "id" to updateTaskTitleDto.id, + ) + ) + } + + override suspend fun updateTaskDone(updateTaskDoneDto: UpdateTaskDoneDto) { + dittoManager.getDitto()?.store?.execute( + QUERY_UPDATE_TASK_DONE, + mapOf( + "id" to updateTaskDoneDto.id, + "done" to updateTaskDoneDto.done, + ) + ) + } + + override suspend fun removeTask(taskId: String) { + dittoManager.getDitto()?.store?.execute( + QUERY_UPDATE_TASK_DELETED, + mapOf("id" to taskId) + ) + } + + override fun onCleared() { + syncSubscription?.close() + syncSubscription = null + } + + private fun DittoQueryResultItem.toTask(): Task = Task( + id = this.value["_id"].string, + title = this.value["title"].string, + done = this.value["done"].boolean, + deleted = this.value["deleted"].boolean, + ) + + // Register a subscription, which determines what data syncs to this device. + // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions + private suspend fun registerSubscription() { + if (syncSubscription != null) return + syncSubscription = + dittoManager.getDitto()?.sync?.registerSubscription(SUBSCRIPTION_QUERY_SELECT_TASKS) + } +} diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/screenstate/MainScreenState.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/screenstate/MainScreenState.kt index 946d6a625..ebd8cfcf4 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/screenstate/MainScreenState.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/screenstate/MainScreenState.kt @@ -1,7 +1,7 @@ package com.ditto.quickstart.data.screenstate data class MainScreenState( - val appId: String, + val databaseId: String, val appToken: String, val isLoading: Boolean, val isSyncEnabled: Boolean, @@ -9,7 +9,7 @@ data class MainScreenState( ) { companion object { fun initial(): MainScreenState = MainScreenState( - appId = "", + databaseId = "", appToken = "", isLoading = true, isSyncEnabled = true, diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/RepositoryModule.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/RepositoryModule.kt index bb33443e6..38c9d917d 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/RepositoryModule.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/RepositoryModule.kt @@ -1,14 +1,14 @@ package com.ditto.quickstart.di -import com.ditto.quickstart.data.repository.DittoTaskRepository import com.ditto.quickstart.data.repository.PersistentPreferenceRepository import com.ditto.quickstart.data.repository.PreferenceRepository import com.ditto.quickstart.data.repository.TaskRepository +import com.ditto.quickstart.data.repository.TasksRepository import org.koin.dsl.module fun repositoryModule() = module { single { - DittoTaskRepository(get()) + TasksRepository(get()) } single { diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt index 49e137614..52e0a11b9 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt @@ -7,28 +7,24 @@ import com.ditto.kotlin.DittoConfig import com.ditto.kotlin.DittoLog import com.ditto.kotlin.DittoLogLevel import com.ditto.kotlin.DittoLogger -import com.ditto.kotlin.DittoQueryResult -import com.ditto.kotlin.DittoSyncSubscription -import com.ditto.kotlin.error.DittoError -import com.ditto.kotlin.serialization.DittoCborSerializable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch private const val TAG = "DittoManager" /** - * Manages a Ditto instance. + * Manages the lifecycle and configuration of a Ditto instance: config, open, auth, + * transport, and starting/stopping sync. It vends a configured, running [Ditto] + * instance (via [getDitto]) for the tasks repository to use directly. * - * In cases where a ViewModel needs to interact with this class, then create a UseCase for it. - * Keeping this class inaccessible from the ViewModel, will prevent the abuse of ditto APIs like: - * "ditto.sync", "ditto.transport", "ditto.store", etc - * - * Because ditto also has a "database" component, it is fine to expose this class to a Repository. + * This class deliberately does NOT wrap Ditto's APIs (store/sync/etc.). The + * repository calls the real Ditto API — e.g. `dittoManager.getDitto()?.store?.execute(...)` + * — so the quickstart shows how to use Ditto directly rather than modeling an + * abstraction layer over it. */ class DittoManager( val secrets: DittoSecretsConfiguration, @@ -47,28 +43,23 @@ class DittoManager( DittoLogger.minimumLogLevel = DittoLogLevel.Info val config = DittoConfig( - databaseId = secrets.DITTO_APP_ID, + databaseId = secrets.DITTO_DATABASE_ID, connect = DittoConfig.Connect.Server( - url = secrets.DITTO_AUTH_URL, + url = secrets.DITTO_SERVER_URL, ), ) createDitto( config = config ).apply { - auth?.setExpirationHandler { ditto, _ -> + auth?.expirationHandler = { ditto, _ -> // Authenticate when a token is expiring val clientInfo = ditto.auth?.login( - token = secrets.DITTO_PLAYGROUND_TOKEN, + token = secrets.DITTO_DEVELOPMENT_TOKEN, provider = DittoAuthenticationProvider.development(), ) DittoLog.d(TAG, "Auth response: $clientInfo") } - updateTransportConfig { config -> - config.peerToPeer.lan.enabled = true - config.peerToPeer.bluetoothLe.enabled = true - config.peerToPeer.wifiAware.enabled = true - } } } catch (e: Throwable) { DittoLog.e(TAG, "Failed to create Ditto instance: $e") @@ -87,7 +78,7 @@ class DittoManager( fun destroyDitto() { closeJob = scope.launch(Dispatchers.IO) { - getDitto()?.stopSync() + getDitto()?.sync?.stop() getDitto()?.close() ditto = null } @@ -95,42 +86,14 @@ class DittoManager( suspend fun startSync() { val ditto = getDitto() ?: return - ditto.startSync() + ditto.sync.start() } suspend fun stopSync() { - getDitto()?.stopSync() - } - - suspend fun isSyncing() = getDitto()?.isSyncActive == true - - suspend fun executeDql( - query: String, - parameters: DittoCborSerializable.Dictionary = DittoCborSerializable.Dictionary() - ): DittoQueryResult? = try { - getDitto()?.store?.execute(query, parameters) - } catch (e: DittoError) { - DittoLog.e("ExecuteDqlUse", "Error executing DQL query: ${e.message}") - null - } - - suspend fun registerSubscription( - query: String, - arguments: DittoCborSerializable.Dictionary? = null - ): DittoSyncSubscription? = try { - getDitto()?.sync?.registerSubscription(query, arguments) - } catch (e: DittoError) { - DittoLog.e("RegisterSubscription", "Error registering subscription: ${e.message}") - null + getDitto()?.sync?.stop() } - suspend fun registerObserver( - query: String, - arguments: DittoCborSerializable.Dictionary? = null - ): Flow = requireNotNull(getDitto()).store.observe( - query = query, - arguments = arguments - ) + suspend fun isSyncing() = getDitto()?.sync?.isActive == true private suspend fun waitForWorkInProgress() { createJob?.join() diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/MainScreen.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/MainScreen.kt index 586a6b732..dcfb371d9 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/MainScreen.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/MainScreen.kt @@ -50,7 +50,7 @@ fun MainScreen( topBar = { TopBar( isLoading = mainScreenState.isLoading, - appId = mainScreenState.appId, + databaseId = mainScreenState.databaseId, appToken = mainScreenState.appToken, isSyncEnabled = mainScreenState.isSyncEnabled, onSyncChange = mainScreenViewModel::onSyncChange diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/MainScreenViewModel.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/MainScreenViewModel.kt index e7d77bb47..a04c643e7 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/MainScreenViewModel.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/MainScreenViewModel.kt @@ -77,8 +77,8 @@ class MainScreenViewModel( _state.value = _state.value.copy( isLoading = false, isSyncEnabled = isSyncEnabled.await(), - appId = DittoSecretsConfiguration.DITTO_APP_ID, - appToken = DittoSecretsConfiguration.DITTO_PLAYGROUND_TOKEN, + databaseId = DittoSecretsConfiguration.DITTO_DATABASE_ID, + appToken = DittoSecretsConfiguration.DITTO_DEVELOPMENT_TOKEN, errorMessage = errorMessage.await(), ) } diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/TaskListScreenViewModel.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/TaskListScreenViewModel.kt index 55f1cefc3..f3dc695fb 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/TaskListScreenViewModel.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/TaskListScreenViewModel.kt @@ -38,7 +38,7 @@ class TaskListScreenViewModel( ) private fun observerTasksChanges() { - taskRepository.tasksStateFlow + taskRepository.observeTasks() .onEach { _state.value = _state.value.copy(tasks = it) } .launchIn(viewModelScope) } diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/components/TopBar.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/components/TopBar.kt index 64885827b..7c0e9d215 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/components/TopBar.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/components/TopBar.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp @Composable internal fun TopBar( isLoading: Boolean, - appId: String, + databaseId: String, appToken: String, isSyncEnabled: Boolean, onSyncChange: (Boolean) -> Unit, @@ -55,12 +55,12 @@ internal fun TopBar( TopBarAnimatedVisibility(visible = !isLoading) { Row { Text( - text = "App Id:", + text = "Database ID:", style = MaterialTheme.typography.caption.copy(fontWeight = FontWeight.Bold), ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = appId, + text = databaseId, style = MaterialTheme.typography.caption, maxLines = 1, overflow = TextOverflow.Ellipsis diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/usecases/SetSyncStatusUseCase.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/usecases/SetSyncStatusUseCase.kt index 861a39954..726d5e6e9 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/usecases/SetSyncStatusUseCase.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/usecases/SetSyncStatusUseCase.kt @@ -1,7 +1,7 @@ package com.ditto.quickstart.usecases +import com.ditto.kotlin.DittoException import com.ditto.kotlin.DittoLog -import com.ditto.kotlin.error.DittoError import com.ditto.quickstart.data.repository.PreferenceRepository import com.ditto.quickstart.ditto.DittoManager @@ -25,7 +25,7 @@ class SetSyncStatusUseCase( private suspend fun enableSync(): Boolean { try { dittoManager.startSync() - } catch (e: DittoError) { + } catch (e: DittoException) { DittoLog.e(TAG, "Failed to start sync: $e") return false } @@ -43,7 +43,7 @@ class SetSyncStatusUseCase( private suspend fun disableSync(): Boolean { try { dittoManager.stopSync() - } catch (e: DittoError) { + } catch (e: DittoException) { DittoLog.e(TAG, "Failed to stop sync: $e") return false } diff --git a/kotlin-multiplatform/detekt.yml b/kotlin-multiplatform/detekt.yml index 9f029c6e7..d2ecdbdc4 100644 --- a/kotlin-multiplatform/detekt.yml +++ b/kotlin-multiplatform/detekt.yml @@ -142,14 +142,30 @@ complexity: active: false StringLiteralDuplication: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] threshold: 11 thresholdInFiles: 11 thresholdInClasses: 11 @@ -225,7 +241,15 @@ exceptions: - 'toString' InstanceOfCheckForException: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] NotImplementedDeclaration: active: false ObjectExtendsThrowable: @@ -251,7 +275,15 @@ exceptions: active: false ThrowingExceptionsWithoutMessageOrCause: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] exceptions: - 'ArrayIndexOutOfBoundsException' - 'Exception' @@ -266,7 +298,15 @@ exceptions: active: true TooGenericExceptionCaught: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] exceptionNames: - 'ArrayIndexOutOfBoundsException' - 'Error' @@ -312,7 +352,15 @@ naming: minimumFunctionNameLength: 3 FunctionNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] functionPattern: '[a-z][a-zA-Z0-9]*' excludeClassPattern: '$^' ignoreAnnotated: ['Composable'] @@ -373,10 +421,26 @@ performance: threshold: 3 ForEachOnRange: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] SpreadOperator: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] UnnecessaryTemporaryInstantiation: active: true @@ -441,7 +505,15 @@ potential-bugs: active: true LateinitUsage: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] ignoreOnClassesPattern: '' MapGetWithNotNullAssertionOperator: active: true @@ -464,7 +536,15 @@ potential-bugs: active: true UnsafeCallOnNullableType: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] UnsafeCast: active: true UnusedUnaryOperator: @@ -502,7 +582,15 @@ style: active: false ExplicitItLambdaParameter: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] ExpressionBodySyntax: active: false includeLineWrapping: false @@ -561,7 +649,16 @@ style: maxJumpCount: 1 MagicNumber: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + '**/*.kts', + ] ignoreNumbers: - '-1' - '0' @@ -712,6 +809,14 @@ style: ignoreLateinitVar: false WildcardImport: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludes: + [ + '**/test/**', + '**/androidTest/**', + '**/commonTest/**', + '**/jvmTest/**', + '**/jsTest/**', + '**/iosTest/**', + ] excludeImports: - - 'java.util.*' \ No newline at end of file + - 'java.util.*' diff --git a/kotlin-multiplatform/iosApp/iosApp.xcodeproj/project.pbxproj b/kotlin-multiplatform/iosApp/iosApp.xcodeproj/project.pbxproj index 3224de662..f7a990c5a 100644 --- a/kotlin-multiplatform/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/kotlin-multiplatform/iosApp/iosApp.xcodeproj/project.pbxproj @@ -234,7 +234,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/zsh; - shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew -Pditto.skipAndroidBuilds=true :composeApp:embedAndSignAppleFrameworkForXcode\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/react-native-expo/.maestro/flows/01-app-launch-and-seeded-tasks-android.yaml b/react-native-expo/.maestro/flows/01-app-launch-and-seeded-tasks-android.yaml index 31e7fe84f..a1302d5c5 100644 --- a/react-native-expo/.maestro/flows/01-app-launch-and-seeded-tasks-android.yaml +++ b/react-native-expo/.maestro/flows/01-app-launch-and-seeded-tasks-android.yaml @@ -1,6 +1,6 @@ appId: com.anonymous.reactnativeexpo tags: - - "smoke" + - 'smoke' --- # Simple test: Launch app and verify specific task exists using testID # Usage: MAESTRO_DITTO_CLOUD_TASK_TITLE="Basic Test Task" maestro test 01-app-launch-and-seeded-tasks-android.yaml @@ -11,9 +11,9 @@ tags: # Handle permission dialog if it appears - runFlow: when: - visible: "Allow" + visible: 'Allow' commands: - - tapOn: "Allow" + - tapOn: 'Allow' # Wait for app to load - waitForAnimationToEnd @@ -21,7 +21,7 @@ tags: # Debug: Try text selector first to see if task exists - scrollUntilVisible: element: - text: "${MAESTRO_DITTO_CLOUD_TASK_TITLE}" + text: '${MAESTRO_DITTO_CLOUD_TASK_TITLE}' direction: DOWN timeout: 30000 speed: 40 @@ -29,8 +29,8 @@ tags: # Debug: Assert with text selector - assertVisible: - text: "${MAESTRO_DITTO_CLOUD_TASK_TITLE}" + text: '${MAESTRO_DITTO_CLOUD_TASK_TITLE}' # Debug: Also try testID selector - assertVisible: - id: "${MAESTRO_DITTO_CLOUD_TASK_TITLE}" \ No newline at end of file + id: '${MAESTRO_DITTO_CLOUD_TASK_TITLE}' diff --git a/react-native-expo/.maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml b/react-native-expo/.maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml index 8314a3dc7..b794b1962 100644 --- a/react-native-expo/.maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml +++ b/react-native-expo/.maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml @@ -1,6 +1,6 @@ appId: com.anonymous.reactnativeexpo tags: - - "smoke" + - 'smoke' --- # Simple test: Launch app and verify specific task exists using testID # Usage: MAESTRO_DITTO_CLOUD_TASK_TITLE="Basic Test Task" maestro test 01-app-launch-and-seeded-tasks-ios.yaml @@ -11,16 +11,16 @@ tags: # Handle location permission dialog if it appears - runFlow: when: - visible: "Allow" + visible: 'Allow' commands: - - tapOn: "Allow" + - tapOn: 'Allow' # Handle Bluetooth permission dialog if it appears - runFlow: when: - visible: "OK" + visible: 'OK' commands: - - tapOn: "OK" + - tapOn: 'OK' # Wait for app to load - waitForAnimationToEnd @@ -28,7 +28,7 @@ tags: # Scroll until the task is visible using testID - scrollUntilVisible: element: - id: "${MAESTRO_DITTO_CLOUD_TASK_TITLE}" + id: '${MAESTRO_DITTO_CLOUD_TASK_TITLE}' direction: DOWN timeout: 30000 speed: 40 @@ -36,4 +36,4 @@ tags: # Final assertion - task must be visible using id selector - assertVisible: - id: "${MAESTRO_DITTO_CLOUD_TASK_TITLE}" \ No newline at end of file + id: '${MAESTRO_DITTO_CLOUD_TASK_TITLE}' diff --git a/react-native-expo/App.tsx b/react-native-expo/App.tsx index 8172582c9..75dc48e8f 100644 --- a/react-native-expo/App.tsx +++ b/react-native-expo/App.tsx @@ -1,209 +1,46 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState } from 'react'; import { Text, StyleSheet, - PermissionsAndroid, - Platform, View, FlatList, Button, -} from "react-native"; -import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; -import { - Authenticator, - Ditto, - DittoConfig, - DittoConfigConnect, - init, - StoreObserver, - SyncSubscription, -} from "@dittolive/ditto"; -import { - DITTO_APP_ID, - DITTO_PLAYGROUND_TOKEN, - DITTO_AUTH_URL, - DITTO_WEBSOCKET_URL, -} from "@env"; - -import Fab from "./components/Fab"; -import NewTaskModal from "./components/NewTaskModal"; -import DittoInfo from "./components/DittoInfo"; -import DittoSync from "./components/DittoSync"; -import TaskDone from "./components/TaskDone"; -import EditTaskModal from "./components/EditTaskModal"; - -type Task = { - id: string; +} from 'react-native'; +import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; +import { DITTO_DATABASE_ID, DITTO_DEVELOPMENT_TOKEN } from '@env'; + +import Fab from './components/Fab'; +import NewTaskModal from './components/NewTaskModal'; +import DittoInfo from './components/DittoInfo'; +import DittoSync from './components/DittoSync'; +import TaskDone from './components/TaskDone'; +import EditTaskModal from './components/EditTaskModal'; +import { useDitto } from './hooks/useDitto'; +import { useTasks } from './hooks/useTasks'; + +export type Task = { + _id: string; title: string; done: boolean; deleted: boolean; }; - -async function requestPermissions() { - const permissions = [ - PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, - PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE, - PermissionsAndroid.PERMISSIONS.NEARBY_WIFI_DEVICES, - PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, - ]; - - const granted = await PermissionsAndroid.requestMultiple(permissions); - return Object.values(granted).every( - (result) => result === PermissionsAndroid.RESULTS.GRANTED - ); -} - const App = () => { - const ditto = useRef(null); - const taskSubscription = useRef(null); - const taskObserver = useRef(null); + const { ditto, syncEnabled, toggleSync, hasPermissions } = useDitto(); + const { tasks, createTask, toggleTask, deleteTask, updateTaskTitle } = + useTasks(ditto); const [modalVisible, setModalVisible] = useState(false); - const [syncEnabled, setSyncEnabled] = useState(true); const [editingTask, setEditingTask] = useState(null); - const [tasks, setTasks] = useState([]); - - const [hasPermissions, setHasPermissions] = useState(true); - - // https://docs.ditto.live/sdk/latest/sync/start-and-stop-sync - const toggleSync = () => { - if (syncEnabled) { - ditto.current?.sync.stop(); - } else { - ditto.current?.sync.start(); - } - setSyncEnabled(!syncEnabled); - }; - - const createTask = async (title: string) => { - if (title === "") { - return; - } - await ditto.current?.store.execute("INSERT INTO tasks DOCUMENTS (:task)", { - task: { - title, - done: false, - deleted: false, - }, - }); - }; - - const toggleTask = async (task: Task) => { - await ditto.current?.store.execute( - "UPDATE tasks SET done=:done WHERE _id=:id", - { - id: task.id, - done: !task.done, - } - ); - }; - - const deleteTask = async (task: Task) => { - await ditto.current?.store.execute( - "UPDATE tasks SET deleted=true WHERE _id=:id", - { - id: task.id, - } - ); - }; - - const updateTaskTitle = async (taskId: string, newTitle: string) => { - await ditto.current?.store.execute( - "UPDATE tasks SET title=:title WHERE _id=:id", - { - id: taskId, - title: newTitle, - } - ); - }; - - const initDitto = async () => { - try { - await init(); - - // https://docs.ditto.live/sdk/latest/install-guides/react-native#onlineplayground - const databaseId = DITTO_APP_ID; - const playgroundToken = DITTO_PLAYGROUND_TOKEN; - - const connectConfig: DittoConfigConnect = { - mode: 'server', - url: DITTO_AUTH_URL, - }; - - const config = new DittoConfig(databaseId, connectConfig, 'custom-folder'); - - ditto.current = await Ditto.open(config); - - // Configure websocket URL for transport - ditto.current.updateTransportConfig((transportConfig) => { - transportConfig.connect.websocketURLs = [DITTO_WEBSOCKET_URL]; - }); - - if (connectConfig.mode === 'server') { - await ditto.current.auth.setExpirationHandler(async (dittoInstance, timeUntilExpiration) => { - console.log('Authentication expiring soon, time until expiration:', timeUntilExpiration); - - if (dittoInstance.auth.loginSupported) { - const devProvider = Authenticator.DEVELOPMENT_PROVIDER; - const reLoginResult = await dittoInstance.auth.login(playgroundToken, devProvider); - if (reLoginResult.error) { - console.error('Re-authentication failed:', reLoginResult.error); - } else { - console.log('Successfully re-authenticated with info:', reLoginResult); - } - } - }); - - if (ditto.current.auth.loginSupported) { - // Use the development provider constant from Ditto - const devProvider = Authenticator.DEVELOPMENT_PROVIDER; - console.log('Using development provider:', devProvider); - - const loginResult = await ditto.current.auth.login(playgroundToken, devProvider); - if (loginResult.error) { - console.error('Login failed:', loginResult.error); - } else { - console.log('Successfully logged in with info:', loginResult); - } - } - } - - ditto.current.sync.start(); - - await ditto.current.store.execute('ALTER SYSTEM SET DQL_STRICT_MODE = false'); - - taskSubscription.current = ditto.current.sync.registerSubscription('SELECT * FROM tasks'); - - taskObserver.current = ditto.current.store.registerObserver('SELECT * FROM tasks WHERE NOT deleted ORDER BY title ASC', response => { - const fetchedTasks: Task[] = response.items.map(doc => ({ - id: doc.value._id, - title: doc.value.title as string, - done: doc.value.done, - deleted: doc.value.deleted, - })); - - setTasks(fetchedTasks); - }); - } catch (error) { - console.error('Error syncing tasks:', error); - } - }; - - useEffect(() => { - (async () => { - const granted = - Platform.OS === "android" ? await requestPermissions() : true; - - setHasPermissions(granted); - initDitto(); - })(); - }, []); const renderItem = ({ item }: { item: Task }) => ( - + toggleTask(item)} /> - setEditingTask(item)} testID={item.title}> + setEditingTask(item)} + testID={item.title} + > {item.title} @@ -219,40 +56,41 @@ const App = () => { return ( - {!hasPermissions && ( - - - āš ļø Limited functionality: Grant Bluetooth & WiFi permissions for peer-to-peer sync - - - )} - - - setModalVisible(true)} /> - { - createTask(task); - setModalVisible(false); - }} - onClose={() => setModalVisible(false)} - /> - setEditingTask(null)} - onSubmit={(taskId, newTitle) => { - updateTaskTitle(taskId, newTitle); - setEditingTask(null); - }} - onClose={() => setEditingTask(null)} - /> - item.id} - /> + {!hasPermissions && ( + + + āš ļø Limited functionality: Grant Bluetooth & WiFi permissions for + peer-to-peer sync + + + )} + + + setModalVisible(true)} /> + { + createTask(task); + setModalVisible(false); + }} + onClose={() => setModalVisible(false)} + /> + setEditingTask(null)} + onSubmit={(taskId, newTitle) => { + updateTaskTitle(taskId, newTitle); + setEditingTask(null); + }} + onClose={() => setEditingTask(null)} + /> + item._id} + /> ); @@ -260,9 +98,9 @@ const App = () => { const styles = StyleSheet.create({ container: { - height: "100%", + height: '100%', padding: 20, - backgroundColor: "#fff", + backgroundColor: '#fff', }, listContainer: { gap: 5, @@ -270,33 +108,33 @@ const styles = StyleSheet.create({ taskContainer: { flex: 1, gap: 5, - flexDirection: "row", + flexDirection: 'row', paddingVertical: 10, paddingHorizontal: 20, }, taskTitle: { fontSize: 20, - alignSelf: "center", + alignSelf: 'center', flexGrow: 1, flexShrink: 1, }, taskButton: { flexShrink: 1, - alignSelf: "center", + alignSelf: 'center', }, permissionBanner: { - backgroundColor: "#FEF3C7", + backgroundColor: '#FEF3C7', borderWidth: 1, - borderColor: "#D97706", + borderColor: '#D97706', borderRadius: 8, padding: 12, marginBottom: 16, }, permissionText: { - color: "#92400E", + color: '#92400E', fontSize: 14, - textAlign: "center", - fontWeight: "500", + textAlign: 'center', + fontWeight: '500', }, }); diff --git a/react-native-expo/README.md b/react-native-expo/README.md index 0b13b673f..0b89e98f0 100644 --- a/react-native-expo/README.md +++ b/react-native-expo/README.md @@ -7,7 +7,7 @@ Ditto is already installed. ## Requirements -- **Xcode 26.2 or lower** — `@dittolive/ditto@5.0.0-rc.2` bundles `fmt 11.0.2` which has a `consteval` compilation bug with Xcode 26.4's Clang. Builds will fail on Xcode 26.3+. +- **Xcode 26.2 or lower** — the `fmt` library bundled in the Ditto SDK has a `consteval` compilation bug with Xcode 26.4's Clang. Builds will fail on Xcode 26.3+. - **Gradle 9.0+** — Included automatically via the Gradle wrapper. - **JDK 17** — Required for Android builds. Install via `brew install openjdk@17` and ensure `JAVA_HOME` is set. diff --git a/react-native-expo/android/gradle.properties b/react-native-expo/android/gradle.properties index 66b8dac29..b435b9ba6 100644 --- a/react-native-expo/android/gradle.properties +++ b/react-native-expo/android/gradle.properties @@ -28,7 +28,14 @@ android.enablePngCrunchInReleaseBuilds=true # Use this property to specify which architecture you want to build. # You can also override it from the CLI using # ./gradlew -PreactNativeArchitectures=x86_64 -reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 +# +# x86 and x86_64 are omitted here: building all four ABIs causes +# react-native-reanimated's prefab .so files to exhaust disk on CI runners +# before bundleReleaseAar can write the final ZIP. Real devices (and +# BrowserStack device farms) are all ARM, so dropping x86/x86_64 has no +# impact on test coverage. Emulator testing is handled by developer +# workstations, not CI. +reactNativeArchitectures=armeabi-v7a,arm64-v8a # Use this property to enable support to the new architecture. # This will allow you to use TurboModules and the Fabric render in diff --git a/react-native-expo/babel.config.js b/react-native-expo/babel.config.js index dd4e2af52..14df38c6e 100644 --- a/react-native-expo/babel.config.js +++ b/react-native-expo/babel.config.js @@ -1,13 +1,13 @@ module.exports = function (api) { api.cache(false); return { - presets: ["babel-preset-expo"], + presets: ['babel-preset-expo'], plugins: [ [ - "module:react-native-dotenv", + 'module:react-native-dotenv', { - moduleName: "@env", - path: "../.env", + moduleName: '@env', + path: '../.env', safe: false, allowUndefined: true, verbose: false, diff --git a/react-native-expo/components/DittoInfo.tsx b/react-native-expo/components/DittoInfo.tsx index 4d578cf18..eb89b77fe 100644 --- a/react-native-expo/components/DittoInfo.tsx +++ b/react-native-expo/components/DittoInfo.tsx @@ -2,15 +2,15 @@ import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; type Props = { - appId: string, - token: string, -} + databaseId: string; + token: string; +}; -const DittoInfo: React.FC = ({ appId, token }) => { +const DittoInfo: React.FC = ({ databaseId, token }) => { return ( Ditto Tasks (Expo) - {`AppID: ${appId}`} + {`Database ID: ${databaseId}`} {`Token: ${token}`} ); diff --git a/react-native-expo/components/EditTaskModal.tsx b/react-native-expo/components/EditTaskModal.tsx index 2806b8ee6..9d34fa32d 100644 --- a/react-native-expo/components/EditTaskModal.tsx +++ b/react-native-expo/components/EditTaskModal.tsx @@ -1,15 +1,28 @@ import React, { useState, useEffect } from 'react'; -import { Button, Modal, ModalProps, StyleSheet, Text, TextInput, View } from 'react-native'; +import { + Button, + Modal, + ModalProps, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; type EditTaskModalProps = { - task: { id: string, title: string } | null, - onSubmit: (taskId: string, newTitle: string) => void, - onClose?: () => void, -} + task: { _id: string; title: string } | null; + onSubmit: (taskId: string, newTitle: string) => void; + onClose?: () => void; +}; type Props = EditTaskModalProps & ModalProps; -const EditTaskModal: React.FC = ({ task, onSubmit, onClose, ...props }) => { +const EditTaskModal: React.FC = ({ + task, + onSubmit, + onClose, + ...props +}) => { const [input, setInput] = useState(''); useEffect(() => { @@ -20,7 +33,7 @@ const EditTaskModal: React.FC = ({ task, onSubmit, onClose, ...props }) = const submit = () => { if (input !== '' && task) { - onSubmit(task.id, input); + onSubmit(task._id, input); setInput(''); } }; diff --git a/react-native-expo/components/Fab.tsx b/react-native-expo/components/Fab.tsx index 12176b0b1..6d8fd997b 100644 --- a/react-native-expo/components/Fab.tsx +++ b/react-native-expo/components/Fab.tsx @@ -1,5 +1,10 @@ import React from 'react'; -import { StyleSheet, Text, TouchableOpacity, TouchableOpacityProps } from 'react-native'; +import { + StyleSheet, + Text, + TouchableOpacity, + TouchableOpacityProps, +} from 'react-native'; const Fab = (props: TouchableOpacityProps) => { return ( diff --git a/react-native-expo/components/NewTaskModal.tsx b/react-native-expo/components/NewTaskModal.tsx index acaf1bf6c..f8f764584 100644 --- a/react-native-expo/components/NewTaskModal.tsx +++ b/react-native-expo/components/NewTaskModal.tsx @@ -1,10 +1,18 @@ import React, { useState } from 'react'; -import { Button, Modal, ModalProps, StyleSheet, Text, TextInput, View } from 'react-native'; +import { + Button, + Modal, + ModalProps, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; type NewTaskModalProps = { - onSubmit: (taskName: string) => void, - onClose?: () => void, -} + onSubmit: (taskName: string) => void; + onClose?: () => void; +}; type Props = NewTaskModalProps & ModalProps; @@ -23,7 +31,11 @@ const NewTaskModal: React.FC = ({ onSubmit, onClose, ...props }) => { New Task - +