From bb019a43a045331e7a8ca48590306a7f18d692fa Mon Sep 17 00:00:00 2001 From: theplantinthedesk Date: Sat, 24 Aug 2024 13:03:18 +0530 Subject: [PATCH] Resetting --- .browserslistrc | 3 + .dockerignore | 9 + .editorconfig | 33 + .eslintrc.cjs | 96 + .gitattributes | 6 + .github/FUNDING.yml | 1 + .../ISSUE_TEMPLATE/1-bug-report-scripts.yaml | 114 + .../ISSUE_TEMPLATE/2-bug-report-general.yaml | 104 + .../ISSUE_TEMPLATE/3-suggestion-feature.yaml | 73 + .../4-suggestion-new-script.yaml | 133 + .github/ISSUE_TEMPLATE/config.yml | 7 + .github/actions/force-ipv4/README.md | 32 + .github/actions/force-ipv4/action.yml | 12 + .github/actions/force-ipv4/force-ipv4.sh | 80 + .../npm-install-dependencies/action.yml | 12 + .github/actions/setup-node/action.yml | 9 + .github/actions/upload-artifact/action.yaml | 15 + .github/workflows/checks.build.yaml | 109 + .../checks.desktop-runtime-errors.yaml | 76 + .github/workflows/checks.external-urls.yaml | 30 + .github/workflows/checks.quality.yaml | 101 + .github/workflows/checks.scripts.yaml | 87 + .../checks.security.dependencies.yaml | 22 + .github/workflows/checks.security.sast.yaml | 42 + .github/workflows/release.desktop.yaml | 42 + .github/workflows/release.git.yaml | 17 + .github/workflows/release.site.yaml | 126 + .github/workflows/tests.e2e.yaml | 64 + .github/workflows/tests.integration.yaml | 28 + .github/workflows/tests.unit.yaml | 26 + .gitignore | 20 + .markdownlint.json | 4 + .vscode/extensions.json | 26 + CHANGELOG.md | 959 + CONTRIBUTING.md | 80 + Dockerfile | 16 + LICENSE | 661 + README.md | 192 + SECURITY.md | 77 + cypress-dirs.json | 5 + cypress.config.ts | 31 + dist-dirs.json | 5 + docs/application.md | 45 + docs/architecture.md | 81 + docs/ci-cd.md | 45 + docs/collection-files.md | 175 + docs/desktop/desktop-vs-web-features.md | 93 + docs/desktop/system-requirements.md | 36 + docs/development.md | 102 + docs/presentation.md | 131 + docs/research/README.md | 24 + .../windows/01-windows-10-1909-apps.txt | 84 + .../windows/02-windows-10-20H2-apps.txt | 85 + .../windows/03-windows-10-21H2-apps.txt | 85 + .../windows/04-windows-10-22H2-apps.txt | 85 + .../windows/05-windows-11-21H2-apps.txt | 88 + .../windows/06-windows-11-22H2-apps.txt | 91 + .../windows/07-windows-11-23H2-apps.txt | 91 + docs/research/windows/README.md | 46 + docs/script-guidelines.md | 58 + docs/templating.md | 199 + docs/tests.md | 92 + electron-builder.cjs | 74 + electron.vite.config.ts | 92 + img/README.md | 9 + img/architecture/app-ddd.drawio.png | Bin 0 -> 64572 bytes img/architecture/app-state.drawio | 1 + img/architecture/app-state.png | Bin 0 -> 25586 bytes img/architecture/aws-solution.drawio | 1 + img/architecture/aws-solution.png | Bin 0 -> 58590 bytes ...x-preferences-modification-flow.drawio.png | Bin 0 -> 102270 bytes img/architecture/gitops.drawio | 1 + img/architecture/gitops.png | Bin 0 -> 261511 bytes img/logo.svg | 56 + img/screenshot.png | Bin 0 -> 118357 bytes package.json | 106 + pnpm-lock.yaml | 10685 +++++ postcss.config.cjs | 9 + scripts/configure_vscode.py | 190 + scripts/logo-update.js | 234 + scripts/npm-install.js | 199 + scripts/print-dist-dir.js | 58 + scripts/validate-collections-yaml/README.md | 51 + scripts/validate-collections-yaml/__main__.py | 62 + .../requirements.txt | 6 + scripts/verify-build-artifacts.js | 133 + scripts/verify-web-server-status.js | 87 + src/TypeHelpers.ts | 48 + src/application/ApplicationFactory.ts | 21 + src/application/CodeRunner/CodeRunner.ts | 38 + src/application/CodeRunner/ScriptFilename.ts | 1 + src/application/Common/Array.ts | 17 + src/application/Common/CustomError.ts | 54 + src/application/Common/Enum.ts | 62 + src/application/Common/Log/Logger.ts | 6 + .../IScriptingLanguageFactory.ts | 5 + .../ScriptingLanguageFactory.ts | 27 + src/application/Common/Shuffle.ts | 12 + .../Common/Text/FilterEmptyStrings.ts | 25 + src/application/Common/Text/IndentText.ts | 29 + .../Common/Text/SplitTextIntoLines.ts | 11 + .../Common/Timing/BatchedDebounce.ts | 27 + .../Common/Timing/PlatformTimer.ts | 7 + src/application/Common/Timing/Throttle.ts | 164 + src/application/Common/Timing/Timer.ts | 8 + src/application/Context/ApplicationContext.ts | 55 + .../Context/ApplicationContextFactory.ts | 35 + .../Context/IApplicationContext.ts | 20 + .../Context/State/CategoryCollectionState.ts | 51 + .../Context/State/Code/ApplicationCode.ts | 38 + .../State/Code/Event/CodeChangedEvent.ts | 84 + .../State/Code/Event/ICodeChangedEvent.ts | 11 + .../State/Code/Generation/CodeBuilder.ts | 72 + .../Code/Generation/CodeBuilderFactory.ts | 16 + .../State/Code/Generation/ICodeBuilder.ts | 9 + .../Code/Generation/ICodeBuilderFactory.ts | 4 + .../State/Code/Generation/IUserScript.ts | 7 + .../Code/Generation/IUserScriptGenerator.ts | 10 + .../Code/Generation/Languages/BatchBuilder.ts | 21 + .../Code/Generation/Languages/ShellBuilder.ts | 20 + .../Code/Generation/UserScriptGenerator.ts | 84 + .../Context/State/Code/IApplicationCode.ts | 7 + .../State/Code/Position/CodePosition.ts | 25 + .../State/Code/Position/ICodePosition.ts | 5 + .../State/Filter/AdaptiveFilterContext.ts | 35 + .../State/Filter/Event/FilterActionType.ts | 4 + .../State/Filter/Event/FilterChange.ts | 37 + .../State/Filter/Event/FilterChangeDetails.ts | 23 + .../Context/State/Filter/FilterContext.ts | 13 + .../Filter/Result/AppliedFilterResult.ts | 18 + .../State/Filter/Result/FilterResult.ts | 9 + .../State/Filter/Strategy/FilterStrategy.ts | 9 + .../Filter/Strategy/LinearFilterStrategy.ts | 81 + .../Context/State/ICategoryCollectionState.ts | 18 + .../Selection/Category/CategorySelection.ts | 11 + .../Category/CategorySelectionChange.ts | 17 + .../ScriptToCategorySelectionMapper.ts | 60 + .../Script/DebouncedScriptSelection.ts | 176 + .../State/Selection/Script/ScriptSelection.ts | 18 + .../Selection/Script/ScriptSelectionChange.ts | 18 + .../State/Selection/Script/SelectedScript.ts | 7 + .../Selection/Script/UserSelectedScript.ts | 16 + .../Context/State/Selection/UserSelection.ts | 12 + .../State/Selection/UserSelectionFacade.ts | 39 + src/application/IApplicationFactory.ts | 5 + src/application/Parser/ApplicationParser.ts | 48 + .../Parser/CategoryCollectionParser.ts | 75 + .../Parser/Common/ContextualError.ts | 116 + .../Parser/Common/TypeValidator.ts | 131 + .../CategoryCollectionSpecificUtilities.ts | 35 + .../Parser/Executable/CategoryParser.ts | 181 + .../Parser/Executable/DocumentationParser.ts | 54 + .../Expressions/Expression/Expression.ts | 65 + .../Expression/ExpressionEvaluationContext.ts | 16 + .../Expression/ExpressionPosition.ts | 34 + .../Expression/ExpressionPositionFactory.ts | 21 + .../Expressions/Expression/IExpression.ts | 9 + .../Expressions/ExpressionsCompiler.ts | 167 + .../Expressions/IExpressionsCompiler.ts | 8 + .../Parser/CompositeExpressionParser.ts | 23 + .../Expressions/Parser/IExpressionParser.ts | 5 + .../Parser/Regex/ExpressionRegexBuilder.ts | 59 + .../Expressions/Parser/Regex/RegexParser.ts | 127 + .../Expressions/Pipes/IPipelineCompiler.ts | 3 + .../Script/Compiler/Expressions/Pipes/Pipe.ts | 4 + .../PipeDefinitions/EscapeDoubleQuotes.ts | 33 + .../Pipes/PipeDefinitions/InlinePowerShell.ts | 189 + .../Compiler/Expressions/Pipes/PipeFactory.ts | 48 + .../Expressions/Pipes/PipelineCompiler.ts | 31 + .../ParameterSubstitutionParser.ts | 31 + .../Expressions/SyntaxParsers/WithParser.ts | 222 + .../Call/Argument/FunctionCallArgument.ts | 41 + .../FunctionCallArgumentCollection.ts | 35 + .../IFunctionCallArgumentCollection.ts | 11 + .../CodeSegmentJoin/CodeSegmentMerger.ts | 5 + .../NewlineCodeSegmentMerger.ts | 23 + .../Function/Call/Compiler/CompiledCode.ts | 4 + .../FunctionCallCompilationContext.ts | 9 + .../Call/Compiler/FunctionCallCompiler.ts | 10 + .../Compiler/FunctionCallSequenceCompiler.ts | 34 + .../AdaptiveFunctionCallCompiler.ts | 78 + .../Compiler/SingleCall/SingleCallCompiler.ts | 10 + .../SingleCall/SingleCallCompilerStrategy.ts | 13 + .../Strategies/Argument/ArgumentCompiler.ts | 10 + .../NestedFunctionArgumentCompiler.ts | 120 + .../Strategies/InlineFunctionCallCompiler.ts | 48 + .../Strategies/NestedFunctionCallCompiler.ts | 47 + .../Compiler/Function/Call/FunctionCall.ts | 6 + .../Function/Call/FunctionCallsParser.ts | 80 + .../Function/Call/ParsedFunctionCall.ts | 13 + .../Compiler/Function/ISharedFunction.ts | 30 + .../Function/ISharedFunctionCollection.ts | 6 + .../Function/Parameter/FunctionParameter.ts | 4 + .../Parameter/FunctionParameterCollection.ts | 25 + .../FunctionParameterCollectionFactory.ts | 12 + .../Parameter/FunctionParameterParser.ts | 21 + .../Parameter/IFunctionParameterCollection.ts | 9 + .../Function/Shared/ParameterNameValidator.ts | 22 + .../Compiler/Function/SharedFunction.ts | 62 + .../Function/SharedFunctionCollection.ts | 35 + .../Function/SharedFunctionsParser.ts | 217 + .../Script/Compiler/IScriptCompiler.ts | 7 + .../Script/Compiler/ScriptCompiler.ts | 86 + .../Parser/Executable/Script/ScriptParser.ts | 144 + .../Script/Validation/CodeValidator.ts | 46 + .../Executable/Script/Validation/ICodeLine.ts | 4 + .../Script/Validation/ICodeValidationRule.ts | 10 + .../Script/Validation/ICodeValidator.ts | 8 + .../Validation/Rules/NoDuplicatedLines.ts | 45 + .../Script/Validation/Rules/NoEmptyLines.ts | 21 + .../Validation/Syntax/BatchFileSyntax.ts | 10 + .../Validation/Syntax/ILanguageSyntax.ts | 4 + .../Validation/Syntax/ISyntaxFactory.ts | 4 + .../Validation/Syntax/ShellScriptSyntax.ts | 7 + .../Script/Validation/Syntax/SyntaxFactory.ts | 16 + .../Validation/ExecutableErrorContext.ts | 24 + .../ExecutableErrorContextMessage.ts | 43 + .../Executable/Validation/ExecutableType.ts | 4 + .../Validation/ExecutableValidator.ts | 69 + .../Parser/ProjectDetailsParser.ts | 33 + .../ScriptingDefinition/CodeSubstituter.ts | 53 + .../ScriptingDefinitionParser.ts | 55 + src/application/Repository/Repository.ts | 19 + .../Repository/RepositoryEntity.ts | 6 + .../ScriptDiagnosticsCollector.ts | 10 + src/application/collections/.schema.yaml | 197 + src/application/collections/README.md | 13 + .../collections/collection.yaml.d.ts | 82 + src/application/collections/linux.yaml | 4003 ++ src/application/collections/macos.yaml | 2213 + src/application/collections/windows.yaml | 33953 ++++++++++++++++ src/domain/Application.ts | 44 + src/domain/Collection/CategoryCollection.ts | 134 + src/domain/Collection/ICategoryCollection.ts | 20 + .../Validation/CategoryCollectionValidator.ts | 15 + .../CompositeCategoryCollectionValidator.ts | 33 + .../Rules/EnsureKnownOperatingSystem.ts | 9 + ...EnsurePresenceOfAllRecommendationLevels.ts | 35 + .../EnsurePresenceOfAtLeastOneCategory.ts | 9 + .../Rules/EnsurePresenceOfAtLeastOneScript.ts | 9 + .../Rules/EnsureUniqueIdsAcrossExecutables.ts | 43 + src/domain/Executables/Category/Category.ts | 10 + .../Executables/Category/CategoryFactory.ts | 76 + src/domain/Executables/Documentable.ts | 3 + src/domain/Executables/Executable.ts | 6 + src/domain/Executables/Identifiable.ts | 5 + .../Code/DistinctReversibleScriptCode.ts | 31 + .../Executables/Script/Code/ScriptCode.ts | 4 + .../Script/Code/ScriptCodeFactory.ts | 12 + .../Executables/Script/RecommendationLevel.ts | 4 + src/domain/Executables/Script/Script.ts | 11 + .../Executables/Script/ScriptFactory.ts | 51 + src/domain/IApplication.ts | 11 + src/domain/IScriptingDefinition.ts | 8 + src/domain/OperatingSystem.ts | 37 + src/domain/Project/GitHubProjectDetails.ts | 64 + src/domain/Project/ProjectDetails.ts | 15 + src/domain/ScriptingDefinition.ts | 33 + src/domain/ScriptingLanguage.ts | 4 + src/domain/Version.ts | 24 + .../Directory/PersistentDirectoryProvider.ts | 116 + .../Directory/ScriptDirectoryProvider.ts | 23 + .../Creation/Filename/FilenameGenerator.ts | 5 + .../Filename/TimestampedFilenameGenerator.ts | 31 + .../ScriptFileCreationOrchestrator.ts | 122 + .../CodeRunner/Creation/ScriptFileCreator.ts | 31 + .../CommandDefinition/CommandDefinition.ts | 5 + .../Commands/LinuxVisibleTerminalCommand.ts | 61 + .../Commands/MacOsVisibleTerminalCommand.ts | 46 + ...ncodedPowerShellInvokeCmdCommandCreator.ts | 32 + .../PowerShellInvokeShellCommandCreator.ts | 3 + .../PosixShellArgumentEscaper.ts | 18 + .../PowerShellArgumentEscaper.ts | 15 + .../ShellArgument/ShellArgumentEscaper.ts | 3 + .../Commands/WindowsVisibleTerminalCommand.ts | 61 + .../Factory/CommandDefinitionFactory.ts | 5 + .../OsSpecificTerminalLaunchCommandFactory.ts | 40 + .../Runner/CommandDefinitionRunner.ts | 9 + ...cutableFileShellCommandDefinitionRunner.ts | 80 + .../ExecutablePermissionSetter.ts | 5 + .../FileSystemExecutablePermissionSetter.ts | 35 + .../LoggingNodeShellCommandRunner.ts | 47 + .../Runner/ShellRunner/ShellCommandRunner.ts | 23 + .../Execution/ScriptFileExecutor.ts | 22 + .../Execution/VisibleTerminalFileRunner.ts | 71 + .../CodeRunner/ScriptFileCodeRunner.ts | 57 + .../System/NodeElectronSystemOperations.ts | 57 + .../CodeRunner/System/SystemOperations.ts | 25 + .../Dialog/Browser/BrowserDialog.ts | 30 + .../Dialog/Browser/BrowserSaveFileDialog.ts | 9 + .../Dialog/Browser/FileSaverDialog.ts | 42 + .../Dialog/Electron/ElectronDialog.ts | 29 + .../Dialog/Electron/ElectronSaveFileDialog.ts | 9 + .../Electron/NodeElectronSaveFileDialog.ts | 176 + .../Dialog/LoggingDialogDecorator.ts | 36 + .../EnvironmentVariablesFactory.ts | 18 + .../EnvironmentVariablesValidator.ts | 48 + .../EnvironmentVariables/IAppMetadata.ts | 10 + .../IEnvironmentVariables.ts | 9 + .../IEnvironmentVariablesFactory.ts | 5 + .../Vite/ViteEnvironmentKeys.ts | 13 + .../Vite/ViteEnvironmentVariables.ts | 33 + .../EnvironmentVariables/Vite/vite-env.d.ts | 11 + src/infrastructure/Events/EventSource.ts | 27 + .../Events/EventSubscriptionCollection.ts | 27 + src/infrastructure/Events/IEventSource.ts | 9 + .../Events/IEventSubscriptionCollection.ts | 9 + src/infrastructure/Log/ConsoleLogger.ts | 32 + src/infrastructure/Log/ElectronLogger.ts | 9 + src/infrastructure/Log/NoopLogger.ts | 11 + .../Log/WindowInjectedLogger.ts | 32 + .../NodeReadbackFileWriter.ts | 115 + .../ReadbackFileWriter/ReadbackFileWriter.ts | 59 + .../Repository/InMemoryRepository.ts | 57 + .../Browser/BrowserOs/BrowserCondition.ts | 16 + .../Browser/BrowserOs/BrowserConditions.ts | 106 + .../Browser/BrowserOs/BrowserOsDetector.ts | 10 + .../BrowserOs/ConditionBasedOsDetector.ts | 92 + .../Browser/BrowserRuntimeEnvironment.ts | 50 + .../Browser/TouchSupportDetection.ts | 57 + .../ContextIsolatedElectronDetector.ts | 54 + .../Electron/ElectronEnvironmentDetector.ts | 6 + .../RuntimeEnvironment/Node/NodeOsMapper.ts | 16 + .../Node/NodeRuntimeEnvironment.ts | 28 + .../RuntimeEnvironment/RuntimeEnvironment.ts | 7 + .../RuntimeEnvironmentFactory.ts | 32 + .../RuntimeSanity/Common/FactoryValidator.ts | 28 + .../Common/ISanityCheckOptions.ts | 4 + .../RuntimeSanity/Common/ISanityValidator.ts | 7 + .../RuntimeSanity/SanityChecks.ts | 37 + .../EnvironmentVariablesValidator.ts | 20 + .../Validators/WindowVariablesValidator.ts | 15 + .../ScriptEnvironmentDiagnosticsCollector.ts | 20 + src/infrastructure/Threading/AsyncLazy.ts | 54 + src/infrastructure/Threading/AsyncSleep.ts | 11 + .../WindowVariables/WindowVariables.ts | 15 + .../WindowVariablesValidator.ts | 86 + .../WindowVariables/window-variables.d.ts | 6 + src/presentation/README.md | 3 + .../fonts/roboto-mono-v23-latin-regular.woff2 | Bin 0 -> 12764 bytes ...eek-ext_latin_latin-ext_vietnamese-700.ttf | Bin 0 -> 102404 bytes ...k-ext_latin_latin-ext_vietnamese-700.woff2 | Bin 0 -> 43344 bytes ...ext_latin_latin-ext_vietnamese-regular.ttf | Bin 0 -> 101736 bytes ...t_latin_latin-ext_vietnamese-regular.woff2 | Bin 0 -> 41744 bytes ...slabo-27px-v14-latin_latin-ext-regular.ttf | Bin 0 -> 51720 bytes ...abo-27px-v14-latin_latin-ext-regular.woff2 | Bin 0 -> 19068 bytes .../source-code-pro-v23-latin-regular.ttf | Bin 0 -> 22544 bytes .../source-code-pro-v23-latin-regular.woff2 | Bin 0 -> 11048 bytes .../fonts/yesteryear-v18-latin-regular.ttf | Bin 0 -> 45132 bytes .../fonts/yesteryear-v18-latin-regular.woff2 | Bin 0 -> 23008 bytes .../assets/icons/battery-full.svg | 1 + .../assets/icons/battery-half.svg | 1 + src/presentation/assets/icons/circle-info.svg | 1 + src/presentation/assets/icons/copy.svg | 1 + src/presentation/assets/icons/desktop.svg | 1 + .../assets/icons/external-link.svg | 6 + src/presentation/assets/icons/face-smile.svg | 1 + .../assets/icons/file-arrow-down.svg | 1 + src/presentation/assets/icons/floppy-disk.svg | 1 + src/presentation/assets/icons/folder-open.svg | 1 + src/presentation/assets/icons/folder.svg | 1 + src/presentation/assets/icons/github.svg | 1 + src/presentation/assets/icons/globe.svg | 1 + src/presentation/assets/icons/left-right.svg | 1 + src/presentation/assets/icons/lightbulb.svg | 1 + .../assets/icons/magnifying-glass.svg | 1 + src/presentation/assets/icons/play.svg | 1 + src/presentation/assets/icons/rotate-left.svg | 1 + src/presentation/assets/icons/shield.svg | 1 + .../assets/icons/square-check.svg | 1 + src/presentation/assets/icons/tag.svg | 1 + .../assets/icons/triangle-exclamation.svg | 1 + src/presentation/assets/icons/user-secret.svg | 1 + src/presentation/assets/icons/xmark.svg | 1 + src/presentation/assets/styles/_colors.scss | 44 + src/presentation/assets/styles/_fonts.scss | 54 + src/presentation/assets/styles/_media.scss | 5 + src/presentation/assets/styles/_mixins.scss | 135 + src/presentation/assets/styles/_spacing.scss | 16 + .../assets/styles/_typography.scss | 24 + .../assets/styles/_vite-path.scss | 4 + .../assets/styles/base/_code-styling.scss | 56 + .../assets/styles/base/_index.scss | 55 + .../assets/styles/base/_link-styling.scss | 42 + .../assets/styles/base/_margin-padding.scss | 63 + .../base/_prevent-scrollbar-layout-shift.scss | 19 + src/presentation/assets/styles/main.scss | 9 + .../bootstrapping/ApplicationBootstrapper.ts | 26 + .../bootstrapping/Bootstrapper.ts | 5 + .../bootstrapping/DependencyProvider.ts | 124 + .../Modules/AppInitializationLogger.ts | 14 + .../Modules/DependencyBootstrapper.ts | 20 + .../MobileSafariActivePseudoClassEnabler.ts | 104 + .../Modules/RuntimeSanityValidator.ts | 15 + src/presentation/common/Dialog.ts | 36 + src/presentation/components/App.vue | 100 + .../Code/CodeButtons/CodeCopyButton.vue | 31 + .../Code/CodeButtons/CodeRunButton.vue | 70 + .../Code/CodeButtons/IconButton.vue | 108 + .../Code/CodeButtons/Save/CodeSaveButton.vue | 87 + .../Help/InfoTooltipInline.vue | 37 + .../Help/InfoTooltipWrapper.vue | 36 + .../Save/RunInstructions/RunInstructions.vue | 76 + .../RunInstructions/Steps/CopyableCommand.vue | 67 + .../RunInstructions/Steps/InstructionStep.vue | 16 + .../Steps/InstructionSteps.vue | 13 + .../Steps/PlatformInstructionSteps.vue | 50 + .../Steps/Platforms/LinuxInstructions.vue | 160 + .../Steps/Platforms/MacOsInstructions.vue | 119 + .../Steps/Platforms/WindowsInstructions.vue | 159 + .../Code/CodeButtons/ScriptErrorDialog.ts | 239 + .../Code/CodeButtons/TheCodeButtons.vue | 50 + .../components/Code/TheCodeArea.vue | 220 + .../components/Code/ace-importer.ts | 17 + .../components/DevToolkit/DevToolkit.vue | 143 + .../components/DevToolkit/DumpNames.ts | 35 + .../DevToolkit/UseScrollbarGutterWidth.ts | 46 + .../Scripts/Menu/MenuOptionList.vue | 42 + .../Scripts/Menu/MenuOptionListItem.vue | 50 + .../Recommendation/Rating/CircleRating.vue | 46 + .../Recommendation/Rating/RatingCircle.vue | 74 + .../RecommendationDocumentation.vue | 118 + .../RecommendationStatusHandler.ts | 104 + .../RecommendationStatusType.ts | 7 + .../TheRecommendationSelector.vue | 144 + .../Menu/Revert/RevertStatusDocumentation.vue | 78 + .../Menu/Revert/RevertStatusHandler.ts | 79 + .../Scripts/Menu/Revert/RevertStatusType.ts | 6 + .../Scripts/Menu/Revert/TheRevertSelector.vue | 145 + .../components/Scripts/Menu/TheOsChanger.vue | 61 + .../Scripts/Menu/TheScriptsMenu.vue | 110 + .../Scripts/Menu/View/TheViewChanger.vue | 67 + .../components/Scripts/Menu/View/ViewType.ts | 4 + .../Scripts/Slider/HorizontalResizeSlider.vue | 79 + .../Scripts/Slider/SliderHandle.vue | 92 + .../Scripts/Slider/UseDragHandler.ts | 92 + .../Scripts/Slider/UseGlobalCursor.ts | 54 + .../components/Scripts/TheScriptArea.vue | 57 + .../View/Cards/CardExpandTransition.vue | 28 + .../Scripts/View/Cards/CardExpansionArrow.vue | 32 + .../Scripts/View/Cards/CardList.vue | 143 + .../Scripts/View/Cards/CardListItem.vue | 290 + .../View/Cards/CardSelectionIndicator.vue | 63 + .../View/Cards/NonCollapsingDirective.ts | 17 + .../Scripts/View/Cards/card-gap.scss | 3 + .../Scripts/View/TheScriptsView.vue | 170 + .../Documentation/DocumentableNode.vue | 97 + .../Documentation/DocumentationText.vue | 65 + .../ToggleDocumentationButton.vue | 62 + .../Markdown/CompositeMarkdownRenderer.ts | 28 + .../NodeContent/Markdown/MarkdownRenderer.ts | 3 + .../NodeContent/Markdown/MarkdownText.vue | 51 + ...neReferenceLabelsToSuperscriptConverter.ts | 36 + .../Renderers/MarkdownItHtmlRenderer.ts | 40 + .../PlainTextUrlsToHyperlinksConverter.ts | 127 + .../View/Tree/NodeContent/NodeContent.vue | 62 + .../View/Tree/NodeContent/NodeMetadata.ts | 15 + .../View/Tree/NodeContent/NodeTitle.vue | 28 + .../View/Tree/NodeContent/RevertToggle.vue | 66 + .../NodeContent/Reverter/CategoryReverter.ts | 50 + .../Tree/NodeContent/Reverter/Reverter.ts | 7 + .../NodeContent/Reverter/ReverterFactory.ts | 19 + .../NodeContent/Reverter/ScriptReverter.ts | 34 + .../View/Tree/NodeContent/ToggleSwitch.vue | 176 + .../Scripts/View/Tree/ScriptsTree.vue | 96 + .../TreeView/Bindings/TreeInputFilterEvent.ts | 34 + .../TreeView/Bindings/TreeInputNodeData.ts | 8 + .../TreeNodeStateChangedEmittedEvent.ts | 8 + .../TreeView/Node/HierarchicalTreeNode.vue | 193 + .../Node/Hierarchy/HierarchyAccess.ts | 19 + .../Node/Hierarchy/TreeNodeHierarchy.ts | 31 + .../Tree/TreeView/Node/InteractableNode.vue | 83 + .../View/Tree/TreeView/Node/LeafTreeNode.vue | 95 + .../View/Tree/TreeView/Node/NodeCheckbox.vue | 110 + .../Tree/TreeView/Node/State/CheckState.ts | 5 + .../Tree/TreeView/Node/State/StateAccess.ts | 43 + .../TreeView/Node/State/StateDescriptor.ts | 9 + .../Tree/TreeView/Node/State/TreeNodeState.ts | 66 + .../TreeNodeStateTransactionDescriber.ts | 44 + .../View/Tree/TreeView/Node/TreeNode.ts | 16 + .../Tree/TreeView/Node/TreeNodeManager.ts | 21 + .../Node/UseKeyboardInteractionState.ts | 31 + .../View/Tree/TreeView/Node/UseNodeState.ts | 27 + .../Tree/TreeView/Rendering/DelayScheduler.ts | 3 + .../Ordering/CollapsedParentOrderer.ts | 35 + .../Rendering/Ordering/RenderQueueOrderer.ts | 5 + .../Scheduling/NodeRenderingStrategy.ts | 5 + .../Scheduling/TimeoutDelayScheduler.ts | 28 + .../Rendering/UseGradualNodeRendering.ts | 131 + .../Focus/SingleNodeCollectionFocusManager.ts | 21 + .../TreeRoot/Focus/SingleNodeFocusManager.ts | 6 + .../NodeCollection/Query/QueryableNodes.ts | 15 + .../NodeCollection/Query/TreeNodeNavigator.ts | 28 + .../NodeCollection/TreeInputParser.ts | 24 + .../NodeCollection/TreeNodeCollection.ts | 15 + .../TreeNodeInitializerAndUpdater.ts | 23 + .../View/Tree/TreeView/TreeRoot/TreeRoot.ts | 7 + .../View/Tree/TreeView/TreeRoot/TreeRoot.vue | 68 + .../Tree/TreeView/TreeRoot/TreeRootManager.ts | 21 + .../Scripts/View/Tree/TreeView/TreeView.vue | 131 + .../UseAutoUpdateChildrenCheckState.ts | 45 + .../TreeView/UseAutoUpdateParentCheckState.ts | 48 + .../View/Tree/TreeView/UseCurrentTreeNodes.ts | 25 + .../UseLeafNodeCheckedStateUpdater.ts | 43 + .../TreeView/UseNodeStateChangeAggregator.ts | 83 + .../TreeView/UseTreeKeyboardNavigation.ts | 180 + .../View/Tree/TreeView/UseTreeQueryFilter.ts | 202 + .../View/Tree/TreeView/tree-colors.scss | 13 + .../CategoryNodeMetadataConverter.ts | 73 + .../TreeNodeMetadataConverter.ts | 31 + .../UseCollectionSelectionStateUpdater.ts | 58 + .../UseSelectedScriptNodeIds.ts | 25 + .../TreeViewAdapter/UseTreeViewFilterEvent.ts | 86 + .../TreeViewAdapter/UseTreeViewNodeInput.ts | 47 + .../ExpandCollapseTransition.vue | 35 + .../UseExpandCollapseAnimation.ts | 223 + .../components/Shared/FlatButton.vue | 72 + .../Hooks/Clipboard/BrowserClipboard.ts | 15 + .../Shared/Hooks/Clipboard/Clipboard.ts | 3 + .../Shared/Hooks/Clipboard/UseClipboard.ts | 13 + .../Shared/Hooks/Common/LifecycleHook.ts | 8 + .../Hooks/Dialog/ClientDialogFactory.ts | 49 + .../Shared/Hooks/Dialog/UseDialog.ts | 14 + .../Shared/Hooks/Log/ClientLoggerFactory.ts | 53 + .../Shared/Hooks/Log/LoggerFactory.ts | 5 + .../components/Shared/Hooks/Log/UseLogger.ts | 8 + .../components/Shared/Hooks/README.md | 5 + .../Hooks/Resize/UseAnimationFrameLimiter.ts | 41 + .../Shared/Hooks/Resize/UseResizeObserver.ts | 66 + .../Hooks/Resize/UseResizeObserverPolyfill.ts | 33 + .../components/Shared/Hooks/UseApplication.ts | 8 + .../Hooks/UseAutoUnsubscribedEventListener.ts | 86 + .../Shared/Hooks/UseAutoUnsubscribedEvents.ts | 21 + .../components/Shared/Hooks/UseCodeRunner.ts | 9 + .../Shared/Hooks/UseCollectionState.ts | 70 + .../components/Shared/Hooks/UseCurrentCode.ts | 32 + .../Shared/Hooks/UseRuntimeEnvironment.ts | 5 + .../Hooks/UseScriptDiagnosticsCollector.ts | 9 + .../Shared/Hooks/UseUserSelectionState.ts | 48 + .../components/Shared/Icon/AppIcon.vue | 56 + .../components/Shared/Icon/IconName.ts | 27 + .../components/Shared/Icon/UseSvgLoader.ts | 93 + .../ScrollLock/ScrollDomStateAccessor.ts | 21 + .../ScrollLock/UseLockBodyBackgroundScroll.ts | 318 + .../WindowScrollDomStateAccessor.ts | 68 + .../Shared/Modal/Hooks/UseAllTrueWatcher.ts | 37 + .../Modal/Hooks/UseCurrentFocusToggle.ts | 23 + .../Modal/Hooks/UseEscapeKeyListener.ts | 14 + .../Shared/Modal/ModalContainer.vue | 159 + .../components/Shared/Modal/ModalContent.vue | 110 + .../components/Shared/Modal/ModalDialog.vue | 95 + .../components/Shared/Modal/ModalOverlay.vue | 65 + .../components/Shared/OperatingSystemNames.ts | 15 + .../components/Shared/SizeObserver.vue | 86 + .../components/Shared/TooltipWrapper.vue | 280 + .../components/TheFooter/DownloadUrlList.vue | 86 + .../TheFooter/DownloadUrlListItem.vue | 71 + .../components/TheFooter/PrivacyPolicy.vue | 83 + .../components/TheFooter/TheFooter.vue | 145 + src/presentation/components/TheHeader.vue | 63 + src/presentation/components/TheSearchBar.vue | 131 + src/presentation/electron/build/README.md | 13 + src/presentation/electron/build/icon.ico | Bin 0 -> 372526 bytes src/presentation/electron/build/icon.png | Bin 0 -> 51985 bytes .../electron/main/ElectronConfig.ts | 16 + .../electron/main/IpcRegistration.ts | 44 + .../main/Update/AutomaticUpdateCoordinator.ts | 76 + .../main/Update/ElectronAutoUpdaterFactory.ts | 8 + .../main/Update/ManualUpdater/Dialogs.ts | 122 + .../main/Update/ManualUpdater/Downloader.ts | 222 + .../main/Update/ManualUpdater/Installer.ts | 17 + .../main/Update/ManualUpdater/Integrity.ts | 38 + .../ManualUpdater/ManualUpdateCoordinator.ts | 154 + .../ManualUpdater/RetryFileSystemAccess.ts | 39 + .../electron/main/Update/UpdateInitializer.ts | 49 + .../electron/main/Update/UpdateProgressBar.ts | 95 + src/presentation/electron/main/index.ts | 194 + .../ContextBridging/ApiContextBridge.ts | 17 + .../ContextBridging/MethodContextBinder.ts | 52 + .../preload/ContextBridging/README.md | 14 + .../ContextBridging/RendererApiProvider.ts | 36 + .../ContextBridging/SecureFacadeCreator.ts | 42 + src/presentation/electron/preload/index.ts | 19 + .../electron/shared/IpcBridging/IpcChannel.ts | 6 + .../IpcBridging/IpcChannelDefinitions.ts | 23 + .../electron/shared/IpcBridging/IpcProxy.ts | 68 + .../electron/shared/IpcBridging/README.md | 17 + src/presentation/index.html | 67 + src/presentation/injectionSymbols.ts | 94 + src/presentation/main.ts | 10 + src/presentation/public/favicon.ico | Bin 0 -> 372526 bytes src/presentation/public/icon.png | Bin 0 -> 24057 bytes src/presentation/public/robots.txt | 2 + tests/.eslintrc.cjs | 5 + tests/README.md | 3 + .../check-desktop-runtime-errors/README.md | 29 + .../app/app-logs.ts | 91 + .../app/check-for-errors.ts | 177 + .../app/error-ignore-patterns.ts | 41 + .../extractors/common/app-artifact-locator.ts | 46 + .../extractors/common/extraction-result.ts | 4 + .../app/extractors/linux.ts | 40 + .../app/extractors/macos.ts | 86 + .../app/extractors/windows.ts | 58 + .../app/runner.ts | 201 + .../app/system-capture/screen-capture.ts | 63 + .../system-capture/window-title-capture.ts | 124 + .../check-desktop-runtime-errors/cli-args.ts | 36 + .../check-desktop-runtime-errors/config.ts | 13 + .../check-desktop-runtime-errors/index.ts | 3 + .../check-desktop-runtime-errors/main.ts | 72 + .../check-desktop-runtime-errors/utils/io.ts | 21 + .../check-desktop-runtime-errors/utils/log.ts | 68 + .../check-desktop-runtime-errors/utils/npm.ts | 104 + .../utils/platform.ts | 31 + .../utils/run-command.ts | 58 + .../utils/sleep.ts | 5 + .../desktop-runtime-errors/main.spec.ts | 88 + .../DocumentationUrlExtractor.ts | 69 + .../StatusChecker/BatchStatusChecker.ts | 78 + .../ExponentialBackOffRetryHandler.ts | 56 + .../StatusChecker/FetchFollow.ts | 122 + .../StatusChecker/FetchWithTimeout.ts | 14 + .../external-urls/StatusChecker/README.md | 109 + .../external-urls/StatusChecker/Requestor.ts | 123 + .../StatusChecker/TlsFingerprintRandomizer.ts | 69 + .../StatusChecker/UrlDomainProcessing.ts | 19 + .../external-urls/StatusChecker/UrlStatus.ts | 19 + .../external-urls/StatusChecker/UserAgents.ts | 30 + .../TestExecutionDetailsLogger.ts | 26 + tests/checks/external-urls/main.spec.ts | 100 + tests/e2e/.eslintrc.cjs | 14 + tests/e2e/.gitignore | 2 + .../card-list-layout-stability-on-load.cy.ts | 226 + tests/e2e/code-highlighting.cy.ts | 40 + tests/e2e/initialization.cy.ts | 41 + tests/e2e/no-unintended-layout-shifts.cy.ts | 79 + tests/e2e/no-unintended-overflow.cy.ts | 25 + tests/e2e/operating-system-selector.cy.ts | 69 + tests/e2e/revert-toggle.cy.ts | 79 + tests/e2e/support/assert/layout-stability.ts | 99 + tests/e2e/support/commands.ts | 2 + tests/e2e/support/e2e.ts | 30 + tests/e2e/support/interactions/card.ts | 7 + tests/e2e/support/interactions/code-area.ts | 7 + tests/e2e/support/interactions/header.ts | 3 + .../support/interactions/script-selection.ts | 15 + .../scenarios/viewport-test-scenarios.ts | 23 + tests/e2e/tsconfig.json | 18 + .../Parser/ApplicationParser.spec.ts | 13 + .../Shared/ParameterNameValidator.spec.ts | 51 + .../composite/DependencyResolution.spec.ts | 54 + tests/integration/composite/README.md | 3 + .../CodeRunner/ScriptFileCodeRunner.spec.ts | 100 + .../EnvironmentVariablesFactory.spec.ts | 15 + .../Vite/ViteEnvironmentVariables.spec.ts | 48 + .../BrowserOs/BrowserOsTestCases.ts | 265 + .../ConditionBasedOsDetector.spec.ts | 30 + .../RuntimeEnvironmentFactory.spec.ts | 23 + .../RuntimeSanity/SanityChecks.spec.ts | 63 + .../EnvironmentVariablesValidator.spec.ts | 7 + .../Validators/ValidatorTestRunner.ts | 15 + .../WindowVariablesValidator.spec.ts | 7 + .../ApplicationBootstrapper.spec.ts | 16 + ...bileSafariActivePseudoClassEnabler.spec.ts | 66 + .../Modules/MobileSafariDetectionTestCases.ts | 221 + .../CompositeMarkdownRenderer.spec.ts | 244 + .../Tree/Shared/Icon/UseSvgLoader.spec.ts | 20 + .../Modal/Hooks/UseEscapeKeyListener.spec.ts | 74 + .../Tree/Shared/OperatingSystemNames.spec.ts | 24 + .../Node/UseKeyboardInteractionState.spec.ts | 62 + .../View/Tree/TreeView/TreeView.spec.ts | 158 + .../UseAutoUnsubscribedEventListener.spec.ts | 67 + .../Shared/Modal/ModalContainer.spec.ts | 29 + .../ContextBridging/ApiContextBridge.spec.ts | 15 + .../RendererApiProvider.spec.ts | 66 + .../shared/TestCases/TouchSupportOptions.ts | 37 + tests/shared/Assertions/ExpectDeepIncludes.ts | 47 + .../Assertions/ExpectDeepThrowsError.ts | 51 + tests/shared/Assertions/ExpectExists.ts | 18 + tests/shared/Assertions/ExpectThrowsAsync.ts | 29 + tests/shared/Assertions/ExpectTrue.ts | 18 + tests/shared/FormatAssertionMessage.ts | 7 + tests/shared/HtmlParser.ts | 5 + tests/shared/Spies/EventTargetSpy.ts | 70 + .../TestCases/SupportedOperatingSystems.ts | 11 + .../Vue/ExecuteInComponentSetupContext.ts | 27 + tests/shared/Vue/WaitForValueChange.ts | 20 + tests/shared/bootstrap/BlobPolyfill.ts | 7 + .../bootstrap/FailTestOnConsoleError.ts | 23 + tests/shared/bootstrap/setup.ts | 8 + .../application/ApplicationFactory.spec.ts | 48 + .../Common/Array.ComparerTestScenario.ts | 74 + tests/unit/application/Common/Array.spec.ts | 42 + .../application/Common/CustomError.spec.ts | 174 + tests/unit/application/Common/Enum.spec.ts | 112 + .../application/Common/EnumRangeTestRunner.ts | 46 + .../ScriptingLanguageFactory.spec.ts | 53 + .../ScriptingLanguageFactoryTestRunner.ts | 87 + tests/unit/application/Common/Shuffle.spec.ts | 52 + .../Common/Text/FilterEmptyStrings.spec.ts | 99 + .../Common/Text/IndentText.spec.ts | 130 + .../Common/Text/SplitTextIntoLines.spec.ts | 96 + .../Common/Timing/BatchedDebounce.spec.ts | 144 + .../Common/Timing/PlatformTimer.spec.ts | 78 + .../Common/Timing/Throttle.spec.ts | 325 + .../Context/ApplicationContext.spec.ts | 261 + .../Context/ApplicationContextFactory.spec.ts | 102 + .../State/CategoryCollectionState.spec.ts | 167 + .../State/Code/ApplicationCode.spec.ts | 210 + .../State/Code/Event/CodeChangedEvent.spec.ts | 262 + .../State/Code/Generation/CodeBuilder.spec.ts | 197 + .../Generation/CodeBuilderFactory.spec.ts | 14 + .../Generation/Languages/BatchBuilder.spec.ts | 74 + .../Generation/Languages/ShellBuilder.spec.ts | 69 + .../Generation/UserScriptGenerator.spec.ts | 274 + .../State/Code/Position/CodePosition.spec.ts | 53 + .../Filter/AdaptiveFilterContext.spec.ts | 130 + .../State/Filter/Event/FilterChange.spec.ts | 110 + .../Filter/Result/AppliedFilterResult.spec.ts | 104 + .../Strategy/LinearFilterStrategy.spec.ts | 281 + .../ScriptToCategorySelectionMapper.spec.ts | 272 + .../Script/DebouncedScriptSelection.spec.ts | 658 + .../Script/ExpectEqualSelectedScripts.ts | 48 + .../Script/UserSelectedScript.spec.ts | 27 + .../Selection/UserSelectionFacade.spec.ts | 133 + .../Parser/ApplicationParser.spec.ts | 197 + .../Parser/CategoryCollectionParser.spec.ts | 316 + .../Parser/Common/ContextualError.spec.ts | 191 + .../Parser/Common/ContextualErrorTester.ts | 53 + .../Parser/Common/TypeValidator.spec.ts | 259 + ...ategoryCollectionSpecificUtilities.spec.ts | 103 + .../Parser/Executable/CategoryParser.spec.ts | 530 + .../Executable/DocumentationParser.spec.ts | 73 + .../Expressions/Expression/Expression.spec.ts | 240 + .../ExpressionEvaluationContext.spec.ts | 57 + .../Expression/ExpressionPosition.spec.ts | 213 + .../ExpressionPositionFactory.spec.ts | 99 + .../Expressions/ExpressionsCompiler.spec.ts | 330 + .../Parser/CompositeExpressionParser.spec.ts | 89 + .../Regex/ExpressionRegexBuilder.spec.ts | 428 + .../Parser/Regex/RegexParser.spec.ts | 438 + .../EscapeDoubleQuotes.spec.ts | 33 + .../PipeDefinitions/InlinePowerShell.spec.ts | 50 + .../CommonInlinePowerShellTestUtilities.ts | 26 + .../CreateAbsentCodeTests.ts | 28 + .../CreateCommentedCodeTests.ts | 122 + .../CreateDoUntilTests.ts | 407 + .../CreateDoWhileTests.ts | 281 + .../CreateForLoopTests.ts | 214 + .../CreateForeachTests.ts | 284 + .../CreateFunctionTests.ts | 229 + .../CreateHereStringTests.ts | 240 + .../CreateIfStatementTests.ts | 426 + .../CreateLineContinuationBacktickTests.ts | 61 + .../CreateNewlineTests.ts | 103 + .../CreateScriptBlockTests.ts | 255 + .../CreateSwitchTests.ts | 330 + .../CreateTryCatchFinallyTests.ts | 451 + .../InlinePowerShellTests/CreateWhileTests.ts | 222 + .../Pipes/PipeDefinitions/PipeTestRunner.ts | 71 + .../Expressions/Pipes/PipeFactory.spec.ts | 95 + .../Pipes/PipelineCompiler.spec.ts | 132 + .../ParameterSubstitutionParser.spec.ts | 67 + .../SyntaxParsers/SyntaxParserTestsRunner.ts | 163 + .../SyntaxParsers/WithParser.spec.ts | 272 + .../Argument/FunctionCallArgument.spec.ts | 110 + .../FunctionCallArgumentCollection.spec.ts | 138 + .../NewlineCodeSegmentMerger.spec.ts | 104 + .../FunctionCallSequenceCompiler.spec.ts | 223 + .../NestedFunctionCallCompiler.spec.ts | 294 + .../AdaptiveFunctionCallCompiler.spec.ts | 260 + .../NestedFunctionArgumentCompiler.spec.ts | 311 + .../InlineFunctionCallCompiler.spec.ts | 145 + .../Function/Call/FunctionCallsParser.spec.ts | 207 + .../Function/Call/ParsedFunctionCall.spec.ts | 68 + .../Function/ExpectFunctionBodyType.ts | 28 + .../FunctionParameterCollection.spec.ts | 35 + ...FunctionParameterCollectionFactory.spec.ts | 23 + .../Parameter/FunctionParameterParser.spec.ts | 83 + .../Compiler/Function/SharedFunction.spec.ts | 240 + .../Function/SharedFunctionCollection.spec.ts | 77 + .../Function/SharedFunctionsParser.spec.ts | 513 + .../Compiler/ParameterNameValidator.spec.ts | 24 + .../Script/Compiler/ScriptCompiler.spec.ts | 332 + .../Executable/Script/ScriptParser.spec.ts | 546 + .../Script/Validation/CodeValidator.spec.ts | 182 + .../Rules/CodeValidationRuleTestRunner.ts | 36 + .../Rules/NoDuplicatedLines.spec.ts | 78 + .../Validation/Rules/NoEmptyLines.spec.ts | 46 + .../Syntax/ConcreteSyntaxes.spec.ts | 31 + .../Validation/Syntax/SyntaxFactory.spec.ts | 14 + .../DataValidationTestScenarioGenerator.ts | 36 + .../ExecutableErrorContextMessage.spec.ts | 194 + .../Validation/ExecutableValidationTester.ts | 192 + .../Validation/ExecutableValidator.spec.ts | 248 + .../Parser/ProjectDetailsParser.spec.ts | 113 + .../CodeSubstituter.spec.ts | 120 + .../ScriptingDefinitionParser.spec.ts | 146 + .../collections/NoUnintentedInlining.spec.ts | 82 + .../application/collections/raw-loader.d.ts | 4 + tests/unit/domain/Application.spec.ts | 111 + .../Collection/CategoryCollection.spec.ts | 248 + ...mpositeCategoryCollectionValidator.spec.ts | 170 + .../Rules/EnsureKnownOperatingSystem.spec.ts | 21 + ...ePresenceOfAllRecommendationLevels.spec.ts | 99 + ...EnsurePresenceOfAtLeastOneCategory.spec.ts | 39 + .../EnsurePresenceOfAtLeastOneScript.spec.ts | 39 + .../EnsureUniqueIdsAcrossExecutables.spec.ts | 146 + .../Category/CategoryFactory.spec.ts | 316 + .../Script/Code/ScriptCode.spec.ts | 105 + .../Executables/Script/ScriptFactory.spec.ts | 163 + .../Project/GitHubProjectDetails.spec.ts | 216 + tests/unit/domain/ScriptCodeFactory.spec.ts | 52 + tests/unit/domain/ScriptingDefinition.spec.ts | 131 + tests/unit/domain/Version.spec.ts | 110 + .../PersistentDirectoryProvider.spec.ts | 211 + .../TimestampedFilenameGenerator.spec.ts | 124 + .../ScriptFileCreationOrchestrator.spec.ts | 341 + .../LinuxVisibleTerminalCommand.spec.ts | 87 + .../MacOsVisibleTerminalCommand.spec.ts | 74 + ...dPowerShellInvokeCmdCommandCreator.spec.ts | 51 + .../PosixShellArgumentEscaper.spec.ts | 18 + .../PowerShellArgumentEscaper.spec.ts | 23 + .../ShellArgumentEscaperTestRunner.ts | 23 + .../WindowsVisibleTerminalCommand.spec.ts | 135 + ...ecificTerminalLaunchCommandFactory.spec.ts | 74 + ...leFileShellCommandDefinitionRunner.spec.ts | 259 + ...leSystemExecutablePermissionSetter.spec.ts | 151 + .../LoggingNodeShellCommandRunner.spec.ts | 160 + .../VisibleTerminalFileRunner.spec.ts | 263 + .../CodeRunner/ScriptFileCodeRunner.spec.ts | 228 + .../Dialog/Browser/BrowserDialog.spec.ts | 108 + .../Dialog/Browser/FileSaverDialog.spec.ts | 126 + .../Dialog/Electron/ElectronDialog.spec.ts | 104 + .../ElectronFileDialogOperationsStub.ts | 45 + .../NodeElectronSaveFileDialog.spec.ts | 437 + .../Dialog/Electron/NodePathOperationsStub.ts | 21 + .../Dialog/LoggingDialogDecorator.spec.ts | 163 + .../EnvironmentVariablesFactory.ts | 55 + .../EnvironmentVariablesValidator.spec.ts | 76 + .../Vite/ViteEnvironmentKeys.spec.ts | 13 + .../Vite/ViteEnvironmentVariables.spec.ts | 73 + .../infrastructure/Events/EventSource.spec.ts | 90 + .../EventSubscriptionCollection.spec.ts | 154 + .../infrastructure/InMemoryRepository.spec.ts | 184 + .../infrastructure/Log/ConsoleLogger.spec.ts | 67 + .../infrastructure/Log/ElectronLogger.spec.ts | 77 + .../infrastructure/Log/LoggerTestRunner.ts | 25 + .../infrastructure/Log/NoopLogger.spec.ts | 20 + .../Log/WindowInjectedLogger.spec.ts | 50 + .../FileReadWriteOperationsStub.ts | 34 + .../NodeReadbackFileWriter.spec.ts | 299 + .../ConditionBasedOsDetector.spec.ts | 260 + .../Browser/BrowserRuntimeEnvironment.spec.ts | 172 + .../Browser/TouchSupportDetection.spec.ts | 59 + .../ContextIsolatedElectronDetector.spec.ts | 164 + .../Node/NodeOsMapper.spec.ts | 61 + .../Node/NodeRuntimeEnvironment.spec.ts | 146 + .../RuntimeEnvironmentFactory.spec.ts | 148 + .../WindowVariablesValidator.spec.ts | 239 + .../Common/FactoryValidator.spec.ts | 102 + .../RuntimeSanity/SanityChecks.spec.ts | 164 + .../EnvironmentVariablesValidator.spec.ts | 17 + .../FactoryValidatorConcreteTestRunner.ts | 57 + .../WindowVariablesValidator.spec.ts | 17 + ...iptEnvironmentDiagnosticsCollector.spec.ts | 65 + .../Threading/AsyncLazy.spec.ts | 57 + .../Threading/AsyncSleep.spec.ts | 58 + .../ApplicationBootstrapper.spec.ts | 56 + .../bootstrapping/DependencyProvider.spec.ts | 125 + .../Modules/AppInitializationLogger.spec.ts | 16 + .../Modules/DependencyBootstrapper.spec.ts | 120 + ...bileSafariActivePseudoClassEnabler.spec.ts | 137 + .../Modules/RuntimeSanityValidator.spec.ts | 44 + .../unit/presentation/components/App.spec.ts | 14 + .../Code/CodeButtons/CodeCopyButton.spec.ts | 66 + .../Steps/CopyableCommand.spec.ts | 114 + .../Steps/PlatformInstructionSteps.spec.ts | 70 + .../CodeButtons/ScriptErrorDialog.spec.ts | 189 + .../Scripts/Menu/MenuOptionList.spec.ts | 54 + .../Rating/CircleRating.spec.ts | 84 + .../Rating/RatingCircle.spec.ts | 101 + .../RecommendationDocumentation.spec.ts | 148 + .../RecommendationStatusHandler.spec.ts | 168 + .../RecommendationStatusTestScenario.ts | 51 + .../Menu/Revert/RevertStatusHandler.spec.ts | 201 + .../Scripts/Slider/UseDragHandler.spec.ts | 297 + .../Scripts/Slider/UseGlobalCursor.spec.ts | 106 + .../Scripts/View/Cards/CardList.spec.ts | 65 + .../View/Cards/NonCollapsingDirective.spec.ts | 55 + .../Scripts/View/TheScriptsView.spec.ts | 425 + .../CompositeMarkdownRenderer.spec.ts | 104 + ...erenceLabelsToSuperscriptConverter.spec.ts | 156 + .../Modifiers/MarkdownItHtmlRenderer.spec.ts | 98 + .../Modifiers/MarkdownRenderingTester.ts | 11 + ...PlainTextUrlsToHyperlinksConverter.spec.ts | 134 + .../Reverter/CategoryReverter.spec.ts | 180 + .../Reverter/ReverterFactory.spec.ts | 47 + .../Reverter/ScriptReverter.spec.ts | 120 + .../Tree/NodeContent/ToggleSwitch.spec.ts | 274 + .../Bindings/TreeViewFilterEvent.spec.ts | 41 + .../Node/Hierarchy/TreeNodeHierarchy.spec.ts | 115 + .../TreeView/Node/State/TreeNodeState.spec.ts | 175 + .../TreeNodeStateTransactionDescriber.spec.ts | 74 + .../TreeView/Node/TreeNodeManager.spec.ts | 75 + .../Node/UseKeyboardInteractionState.spec.ts | 81 + .../Tree/TreeView/Node/UseNodeState.spec.ts | 84 + .../Ordering/CollapsedParentOrderer.spec.ts | 165 + .../Scheduling/TimeoutDelayScheduler.spec.ts | 86 + .../Rendering/UseGradualNodeRendering.spec.ts | 374 + .../SingleNodeCollectionFocusManager.spec.ts | 92 + .../Query/TreeNodeNavigator.spec.ts | 108 + .../NodeCollection/TreeInputParser.spec.ts | 84 + .../TreeNodeInitializerAndUpdater.spec.ts | 72 + .../Tree/TreeView/TreeRoot/TreeRootManager.ts | 53 + .../UseAutoUpdateChildrenCheckState.spec.ts | 218 + .../UseAutoUpdateParentCheckState.spec.ts | 206 + .../Tree/TreeView/UseCurrentTreeNodes.spec.ts | 86 + .../UseNodeStateChangeAggregator.spec.ts | 344 + .../CategoryNodeMetadataConverter.spec.ts | 173 + .../TreeNodeMetadataConverter.spec.ts | 90 + ...UseCollectionSelectionStateUpdater.spec.ts | 179 + .../UseSelectedScriptNodeIds.spec.ts | 97 + .../UseTreeViewFilterEvent.spec.ts | 282 + .../UseTreeViewNodeInput.spec.ts | 197 + .../UseExpandCollapseAnimation.spec.ts | 97 + .../components/Shared/FlatButton.spec.ts | 167 + .../Hooks/Clipboard/BrowserClipboard.spec.ts | 74 + .../Hooks/Clipboard/UseClipboard.spec.ts | 68 + .../Hooks/Dialog/ClientDialogFactory.spec.ts | 138 + .../Shared/Hooks/Dialog/UseDialog.spec.ts | 19 + .../Hooks/Log/ClientLoggerFactory.spec.ts | 159 + .../Shared/Hooks/Log/UseLogger.spec.ts | 18 + .../Shared/Hooks/UseApplication.spec.ts | 30 + .../UseAutoUnsubscribedEventListener.spec.ts | 177 + .../Hooks/UseAutoUnsubscribedEvents.spec.ts | 86 + .../Shared/Hooks/UseCodeRunner.spec.ts | 27 + .../Shared/Hooks/UseCollectionState.spec.ts | 312 + .../Shared/Hooks/UseCurrentCode.spec.ts | 128 + .../Hooks/UseRuntimeEnvironment.spec.ts | 14 + .../UseScriptDiagnosticsCollector.spec.ts | 27 + .../Hooks/UseUserSelectionState.spec.ts | 293 + .../components/Shared/Icon/AppIcon.spec.ts | 117 + .../Shared/Icon/UseSvgLoader.spec.ts | 160 + .../ScrollLock/DomStateChangeTestScenarios.ts | 204 + .../UseLockBodyBackgroundScroll.spec.ts | 135 + .../Modal/Hooks/UseAllTrueWatcher.spec.ts | 163 + .../Modal/Hooks/UseCurrentFocusToggle.spec.ts | 163 + .../Modal/Hooks/UseEscapeKeyListener.spec.ts | 79 + .../Shared/Modal/ModalContainer.spec.ts | 190 + .../Shared/Modal/ModalContent.spec.ts | 103 + .../Shared/Modal/ModalDialog.spec.ts | 87 + .../Shared/Modal/ModalOverlay.spec.ts | 105 + .../Resize/UseAnimationFrameLimiter.spec.ts | 133 + .../Shared/Resize/UseResizeObserver.spec.ts | 119 + .../electron/main/IpcRegistration.spec.ts | 155 + .../ContextBridging/ApiContextBridge.spec.ts | 96 + .../MethodContextBinder.spec.ts | 132 + .../RendererApiProvider.spec.ts | 123 + .../SecureFacadeCreator.spec.ts | 99 + .../shared/IpcChannelDefinitions.spec.ts | 69 + .../electron/shared/IpcProxy.spec.ts | 276 + tests/unit/presentation/injectionSymbols.ts | 78 + tests/unit/shared/ExceptionCollector.ts | 18 + tests/unit/shared/PromiseInspection.ts | 39 + tests/unit/shared/Stubs/AppMetadataStub.ts | 38 + .../unit/shared/Stubs/ApplicationCodeStub.ts | 19 + .../ApplicationContextChangedEventStub.ts | 19 + .../shared/Stubs/ApplicationContextStub.ts | 38 + tests/unit/shared/Stubs/ApplicationStub.ts | 36 + .../unit/shared/Stubs/ArgumentCompilerStub.ts | 37 + .../unit/shared/Stubs/BatchedDebounceStub.ts | 33 + tests/unit/shared/Stubs/BootstrapperStub.ts | 14 + .../unit/shared/Stubs/BrowserConditionStub.ts | 36 + .../shared/Stubs/BrowserEnvironmentStub.ts | 17 + .../shared/Stubs/BrowserOsDetectorStub.ts | 15 + .../Stubs/CategoryCollectionFactoryStub.ts | 23 + .../Stubs/CategoryCollectionParserStub.ts | 40 + ...CategoryCollectionSpecificUtilitiesStub.ts | 22 + .../Stubs/CategoryCollectionStateStub.ts | 79 + .../shared/Stubs/CategoryCollectionStub.ts | 105 + ...CategoryCollectionValidationContextStub.ts | 34 + tests/unit/shared/Stubs/CategoryDataStub.ts | 25 + .../unit/shared/Stubs/CategoryFactoryStub.ts | 18 + tests/unit/shared/Stubs/CategoryParserStub.ts | 34 + .../shared/Stubs/CategorySelectionStub.ts | 34 + tests/unit/shared/Stubs/CategoryStub.ts | 91 + tests/unit/shared/Stubs/ChildProcesssStub.ts | 43 + tests/unit/shared/Stubs/ClipboardStub.ts | 14 + .../unit/shared/Stubs/CodeChangedEventStub.ts | 26 + tests/unit/shared/Stubs/CodeRunnerStub.ts | 9 + .../shared/Stubs/CodeSegmentMergerStub.ts | 16 + .../unit/shared/Stubs/CodeSubstituterStub.ts | 22 + .../shared/Stubs/CodeValidationRuleStub.ts | 18 + tests/unit/shared/Stubs/CodeValidatorStub.ts | 34 + tests/unit/shared/Stubs/CollectionDataStub.ts | 63 + .../Stubs/CommandDefinitionFactoryStub.ts | 16 + .../Stubs/CommandDefinitionRunnerStub.ts | 28 + .../shared/Stubs/CommandDefinitionStub.ts | 33 + tests/unit/shared/Stubs/CommandOpsStub.ts | 25 + tests/unit/shared/Stubs/CompiledCodeStub.ts | 17 + tests/unit/shared/Stubs/DelaySchedulerStub.ts | 19 + tests/unit/shared/Stubs/DialogStub.ts | 23 + .../Stubs/ElectronEnvironmentDetectorStub.ts | 26 + tests/unit/shared/Stubs/EnumParserStub.ts | 31 + .../shared/Stubs/EnvironmentVariablesStub.ts | 11 + .../Stubs/ErrorWithContextWrapperStub.ts | 4 + tests/unit/shared/Stubs/ErrorWrapperStub.ts | 67 + tests/unit/shared/Stubs/EventSourceStub.ts | 22 + .../Stubs/EventSubscriptionCollectionStub.ts | 51 + .../shared/Stubs/EventSubscriptionStub.ts | 35 + .../Stubs/ExecutableErrorContextStub.ts | 11 + .../Stubs/ExecutablePermissionSetterStub.ts | 24 + .../shared/Stubs/ExecutableValidatorStub.ts | 57 + .../Stubs/ExpressionEvaluationContextStub.ts | 22 + .../unit/shared/Stubs/ExpressionParserStub.ts | 29 + tests/unit/shared/Stubs/ExpressionStub.ts | 45 + .../shared/Stubs/ExpressionsCompilerStub.ts | 81 + tests/unit/shared/Stubs/FileSystemOpsStub.ts | 22 + .../shared/Stubs/FilenameGeneratorStub.ts | 22 + .../shared/Stubs/FilterChangeDetailsStub.ts | 35 + .../Stubs/FilterChangeDetailsVisitorStub.ts | 21 + tests/unit/shared/Stubs/FilterContextStub.ts | 55 + tests/unit/shared/Stubs/FilterResultStub.ts | 55 + tests/unit/shared/Stubs/FilterStrategyStub.ts | 24 + .../FunctionCallArgumentCollectionStub.ts | 54 + .../Stubs/FunctionCallArgumentFactoryStub.ts | 10 + .../shared/Stubs/FunctionCallArgumentStub.ts | 17 + .../FunctionCallCompilationContextStub.ts | 27 + .../shared/Stubs/FunctionCallCompilerStub.ts | 56 + .../unit/shared/Stubs/FunctionCallDataStub.ts | 17 + tests/unit/shared/Stubs/FunctionCallStub.ts | 28 + .../shared/Stubs/FunctionCallsParserStub.ts | 30 + tests/unit/shared/Stubs/FunctionCodeStub.ts | 17 + tests/unit/shared/Stubs/FunctionDataStub.ts | 91 + .../Stubs/FunctionParameterCollectionStub.ts | 30 + .../Stubs/FunctionParameterParserStub.ts | 8 + .../shared/Stubs/FunctionParameterStub.ts | 17 + .../unit/shared/Stubs/HierarchyAccessStub.ts | 47 + tests/unit/shared/Stubs/IsArrayStub.ts | 14 + tests/unit/shared/Stubs/IsStringStub.ts | 14 + tests/unit/shared/Stubs/LanguageSyntaxStub.ts | 17 + tests/unit/shared/Stubs/LifecycleHookStub.ts | 31 + tests/unit/shared/Stubs/LocationOpsStub.ts | 49 + tests/unit/shared/Stubs/LoggerStub.ts | 57 + .../unit/shared/Stubs/MarkdownRendererStub.ts | 31 + tests/unit/shared/Stubs/NodeMetadataStub.ts | 26 + .../Stubs/NodeStateChangeEventArgsStub.ts | 48 + .../shared/Stubs/NodeStateChangedEventStub.ts | 19 + .../shared/Stubs/OperatingSystemOpsStub.ts | 21 + .../Stubs/ParameterDefinitionDataStub.ts | 17 + .../Stubs/ParameterNameValidatorStub.ts | 12 + tests/unit/shared/Stubs/PipeFactoryStub.ts | 26 + tests/unit/shared/Stubs/PipeStub.ts | 19 + .../unit/shared/Stubs/PipelineCompilerStub.ts | 10 + ...PowerShellInvokeShellCommandCreatorStub.ts | 24 + .../shared/Stubs/ProjectDetailsParserStub.ts | 22 + tests/unit/shared/Stubs/ProjectDetailsStub.ts | 62 + tests/unit/shared/Stubs/QueryableNodesStub.ts | 28 + .../shared/Stubs/ReadbackFileWriterStub.ts | 29 + .../shared/Stubs/RenderQueueOrdererStub.ts | 8 + .../unit/shared/Stubs/RepositoryEntityStub.ts | 14 + .../shared/Stubs/RuntimeEnvironmentStub.ts | 25 + .../shared/Stubs/SanityCheckOptionsStub.ts | 17 + .../unit/shared/Stubs/SanityValidatorStub.ts | 36 + .../shared/Stubs/ScriptCodeFactoryStub.ts | 22 + tests/unit/shared/Stubs/ScriptCodeStub.ts | 17 + tests/unit/shared/Stubs/ScriptCompilerStub.ts | 28 + tests/unit/shared/Stubs/ScriptDataStub.ts | 82 + .../Stubs/ScriptDiagnosticsCollectorStub.ts | 25 + .../Stubs/ScriptDirectoryProviderStub.ts | 17 + tests/unit/shared/Stubs/ScriptFactoryStub.ts | 18 + .../shared/Stubs/ScriptFileCreatorStub.ts | 27 + .../shared/Stubs/ScriptFileExecutorStub.ts | 16 + tests/unit/shared/Stubs/ScriptParserStub.ts | 37 + .../unit/shared/Stubs/ScriptSelectionStub.ts | 131 + tests/unit/shared/Stubs/ScriptStub.ts | 69 + .../Stubs/ScriptingDefinitionDataStub.ts | 32 + .../shared/Stubs/ScriptingDefinitionStub.ts | 27 + tests/unit/shared/Stubs/SelectedScriptStub.ts | 27 + .../Stubs/SharedFunctionCollectionStub.ts | 33 + tests/unit/shared/Stubs/SharedFunctionStub.ts | 103 + .../shared/Stubs/SharedFunctionsParserStub.ts | 51 + .../shared/Stubs/ShellArgumentEscaperStub.ts | 14 + .../shared/Stubs/ShellCommandRunnerStub.ts | 24 + .../Stubs/SingleCallCompilerStrategyStub.ts | 45 + .../shared/Stubs/SingleCallCompilerStub.ts | 47 + .../Stubs/SingleNodeFocusManagerStub.ts | 10 + tests/unit/shared/Stubs/SizeObserverStub.ts | 27 + .../Stubs/StubWithObservableMethodCalls.ts | 25 + tests/unit/shared/Stubs/SyntaxFactoryStub.ts | 18 + .../unit/shared/Stubs/SystemOperationsStub.ts | 41 + tests/unit/shared/Stubs/ThrottleStub.ts | 34 + tests/unit/shared/Stubs/TimeoutStub.ts | 13 + tests/unit/shared/Stubs/TimerStub.ts | 43 + .../shared/Stubs/TreeInputNodeDataStub.ts | 27 + .../shared/Stubs/TreeNodeCollectionStub.ts | 24 + tests/unit/shared/Stubs/TreeNodeParserStub.ts | 30 + .../shared/Stubs/TreeNodeStateAccessStub.ts | 71 + .../TreeNodeStateChangedEmittedEventStub.ts | 41 + .../Stubs/TreeNodeStateDescriptorStub.ts | 34 + .../Stubs/TreeNodeStateTransactionStub.ts | 32 + tests/unit/shared/Stubs/TreeNodeStub.ts | 46 + tests/unit/shared/Stubs/TreeRootStub.ts | 16 + tests/unit/shared/Stubs/TypeValidatorStub.ts | 61 + tests/unit/shared/Stubs/UseApplicationStub.ts | 19 + .../Stubs/UseAutoUnsubscribedEventsStub.ts | 12 + tests/unit/shared/Stubs/UseClipboardStub.ts | 17 + .../shared/Stubs/UseCollectionStateStub.ts | 132 + tests/unit/shared/Stubs/UseCurrentCodeStub.ts | 17 + .../shared/Stubs/UseCurrentTreeNodesStub.ts | 32 + .../unit/shared/Stubs/UseEventListenerStub.ts | 15 + .../Stubs/UseNodeStateChangeAggregatorStub.ts | 33 + tests/unit/shared/Stubs/UseSvgLoaderStub.ts | 31 + .../shared/Stubs/UseUserSelectionStateStub.ts | 52 + tests/unit/shared/Stubs/UserSelectionStub.ts | 21 + tests/unit/shared/Stubs/VersionStub.ts | 7 + .../Stubs/VueDependencyInjectionApiStub.ts | 18 + .../unit/shared/Stubs/WindowVariablesStub.ts | 57 + tests/unit/shared/TestCases/AbsentTests.ts | 193 + .../shared/TestCases/SingletonFactoryTests.ts | 25 + .../shared/TestCases/TransientFactoryTests.ts | 24 + tsconfig.json | 36 + vite-config-helper.ts | 68 + vite.config.ts | 57 + 1126 files changed, 128921 insertions(+) create mode 100644 .browserslistrc create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .eslintrc.cjs create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml create mode 100644 .github/ISSUE_TEMPLATE/2-bug-report-general.yaml create mode 100644 .github/ISSUE_TEMPLATE/3-suggestion-feature.yaml create mode 100644 .github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/actions/force-ipv4/README.md create mode 100644 .github/actions/force-ipv4/action.yml create mode 100644 .github/actions/force-ipv4/force-ipv4.sh create mode 100644 .github/actions/npm-install-dependencies/action.yml create mode 100644 .github/actions/setup-node/action.yml create mode 100644 .github/actions/upload-artifact/action.yaml create mode 100644 .github/workflows/checks.build.yaml create mode 100644 .github/workflows/checks.desktop-runtime-errors.yaml create mode 100644 .github/workflows/checks.external-urls.yaml create mode 100644 .github/workflows/checks.quality.yaml create mode 100644 .github/workflows/checks.scripts.yaml create mode 100644 .github/workflows/checks.security.dependencies.yaml create mode 100644 .github/workflows/checks.security.sast.yaml create mode 100644 .github/workflows/release.desktop.yaml create mode 100644 .github/workflows/release.git.yaml create mode 100644 .github/workflows/release.site.yaml create mode 100644 .github/workflows/tests.e2e.yaml create mode 100644 .github/workflows/tests.integration.yaml create mode 100644 .github/workflows/tests.unit.yaml create mode 100644 .gitignore create mode 100644 .markdownlint.json create mode 100644 .vscode/extensions.json create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 cypress-dirs.json create mode 100644 cypress.config.ts create mode 100644 dist-dirs.json create mode 100644 docs/application.md create mode 100644 docs/architecture.md create mode 100644 docs/ci-cd.md create mode 100644 docs/collection-files.md create mode 100644 docs/desktop/desktop-vs-web-features.md create mode 100644 docs/desktop/system-requirements.md create mode 100644 docs/development.md create mode 100644 docs/presentation.md create mode 100644 docs/research/README.md create mode 100644 docs/research/windows/01-windows-10-1909-apps.txt create mode 100644 docs/research/windows/02-windows-10-20H2-apps.txt create mode 100644 docs/research/windows/03-windows-10-21H2-apps.txt create mode 100644 docs/research/windows/04-windows-10-22H2-apps.txt create mode 100644 docs/research/windows/05-windows-11-21H2-apps.txt create mode 100644 docs/research/windows/06-windows-11-22H2-apps.txt create mode 100644 docs/research/windows/07-windows-11-23H2-apps.txt create mode 100644 docs/research/windows/README.md create mode 100644 docs/script-guidelines.md create mode 100644 docs/templating.md create mode 100644 docs/tests.md create mode 100644 electron-builder.cjs create mode 100644 electron.vite.config.ts create mode 100644 img/README.md create mode 100644 img/architecture/app-ddd.drawio.png create mode 100644 img/architecture/app-state.drawio create mode 100644 img/architecture/app-state.png create mode 100644 img/architecture/aws-solution.drawio create mode 100644 img/architecture/aws-solution.png create mode 100644 img/architecture/firefox-preferences-modification-flow.drawio.png create mode 100644 img/architecture/gitops.drawio create mode 100644 img/architecture/gitops.png create mode 100644 img/logo.svg create mode 100644 img/screenshot.png create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.cjs create mode 100644 scripts/configure_vscode.py create mode 100644 scripts/logo-update.js create mode 100644 scripts/npm-install.js create mode 100644 scripts/print-dist-dir.js create mode 100644 scripts/validate-collections-yaml/README.md create mode 100644 scripts/validate-collections-yaml/__main__.py create mode 100644 scripts/validate-collections-yaml/requirements.txt create mode 100644 scripts/verify-build-artifacts.js create mode 100644 scripts/verify-web-server-status.js create mode 100644 src/TypeHelpers.ts create mode 100644 src/application/ApplicationFactory.ts create mode 100644 src/application/CodeRunner/CodeRunner.ts create mode 100644 src/application/CodeRunner/ScriptFilename.ts create mode 100644 src/application/Common/Array.ts create mode 100644 src/application/Common/CustomError.ts create mode 100644 src/application/Common/Enum.ts create mode 100644 src/application/Common/Log/Logger.ts create mode 100644 src/application/Common/ScriptingLanguage/IScriptingLanguageFactory.ts create mode 100644 src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts create mode 100644 src/application/Common/Shuffle.ts create mode 100644 src/application/Common/Text/FilterEmptyStrings.ts create mode 100644 src/application/Common/Text/IndentText.ts create mode 100644 src/application/Common/Text/SplitTextIntoLines.ts create mode 100644 src/application/Common/Timing/BatchedDebounce.ts create mode 100644 src/application/Common/Timing/PlatformTimer.ts create mode 100644 src/application/Common/Timing/Throttle.ts create mode 100644 src/application/Common/Timing/Timer.ts create mode 100644 src/application/Context/ApplicationContext.ts create mode 100644 src/application/Context/ApplicationContextFactory.ts create mode 100644 src/application/Context/IApplicationContext.ts create mode 100644 src/application/Context/State/CategoryCollectionState.ts create mode 100644 src/application/Context/State/Code/ApplicationCode.ts create mode 100644 src/application/Context/State/Code/Event/CodeChangedEvent.ts create mode 100644 src/application/Context/State/Code/Event/ICodeChangedEvent.ts create mode 100644 src/application/Context/State/Code/Generation/CodeBuilder.ts create mode 100644 src/application/Context/State/Code/Generation/CodeBuilderFactory.ts create mode 100644 src/application/Context/State/Code/Generation/ICodeBuilder.ts create mode 100644 src/application/Context/State/Code/Generation/ICodeBuilderFactory.ts create mode 100644 src/application/Context/State/Code/Generation/IUserScript.ts create mode 100644 src/application/Context/State/Code/Generation/IUserScriptGenerator.ts create mode 100644 src/application/Context/State/Code/Generation/Languages/BatchBuilder.ts create mode 100644 src/application/Context/State/Code/Generation/Languages/ShellBuilder.ts create mode 100644 src/application/Context/State/Code/Generation/UserScriptGenerator.ts create mode 100644 src/application/Context/State/Code/IApplicationCode.ts create mode 100644 src/application/Context/State/Code/Position/CodePosition.ts create mode 100644 src/application/Context/State/Code/Position/ICodePosition.ts create mode 100644 src/application/Context/State/Filter/AdaptiveFilterContext.ts create mode 100644 src/application/Context/State/Filter/Event/FilterActionType.ts create mode 100644 src/application/Context/State/Filter/Event/FilterChange.ts create mode 100644 src/application/Context/State/Filter/Event/FilterChangeDetails.ts create mode 100644 src/application/Context/State/Filter/FilterContext.ts create mode 100644 src/application/Context/State/Filter/Result/AppliedFilterResult.ts create mode 100644 src/application/Context/State/Filter/Result/FilterResult.ts create mode 100644 src/application/Context/State/Filter/Strategy/FilterStrategy.ts create mode 100644 src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts create mode 100644 src/application/Context/State/ICategoryCollectionState.ts create mode 100644 src/application/Context/State/Selection/Category/CategorySelection.ts create mode 100644 src/application/Context/State/Selection/Category/CategorySelectionChange.ts create mode 100644 src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts create mode 100644 src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts create mode 100644 src/application/Context/State/Selection/Script/ScriptSelection.ts create mode 100644 src/application/Context/State/Selection/Script/ScriptSelectionChange.ts create mode 100644 src/application/Context/State/Selection/Script/SelectedScript.ts create mode 100644 src/application/Context/State/Selection/Script/UserSelectedScript.ts create mode 100644 src/application/Context/State/Selection/UserSelection.ts create mode 100644 src/application/Context/State/Selection/UserSelectionFacade.ts create mode 100644 src/application/IApplicationFactory.ts create mode 100644 src/application/Parser/ApplicationParser.ts create mode 100644 src/application/Parser/CategoryCollectionParser.ts create mode 100644 src/application/Parser/Common/ContextualError.ts create mode 100644 src/application/Parser/Common/TypeValidator.ts create mode 100644 src/application/Parser/Executable/CategoryCollectionSpecificUtilities.ts create mode 100644 src/application/Parser/Executable/CategoryParser.ts create mode 100644 src/application/Parser/Executable/DocumentationParser.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Expression/Expression.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Expression/IExpression.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/IPipelineCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/Pipe.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/EscapeDoubleQuotes.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeFactory.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipelineCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/AdaptiveFunctionCallCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/ISharedFunction.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/SharedFunction.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionCollection.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/IScriptCompiler.ts create mode 100644 src/application/Parser/Executable/Script/Compiler/ScriptCompiler.ts create mode 100644 src/application/Parser/Executable/Script/ScriptParser.ts create mode 100644 src/application/Parser/Executable/Script/Validation/CodeValidator.ts create mode 100644 src/application/Parser/Executable/Script/Validation/ICodeLine.ts create mode 100644 src/application/Parser/Executable/Script/Validation/ICodeValidationRule.ts create mode 100644 src/application/Parser/Executable/Script/Validation/ICodeValidator.ts create mode 100644 src/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines.ts create mode 100644 src/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines.ts create mode 100644 src/application/Parser/Executable/Script/Validation/Syntax/BatchFileSyntax.ts create mode 100644 src/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax.ts create mode 100644 src/application/Parser/Executable/Script/Validation/Syntax/ISyntaxFactory.ts create mode 100644 src/application/Parser/Executable/Script/Validation/Syntax/ShellScriptSyntax.ts create mode 100644 src/application/Parser/Executable/Script/Validation/Syntax/SyntaxFactory.ts create mode 100644 src/application/Parser/Executable/Validation/ExecutableErrorContext.ts create mode 100644 src/application/Parser/Executable/Validation/ExecutableErrorContextMessage.ts create mode 100644 src/application/Parser/Executable/Validation/ExecutableType.ts create mode 100644 src/application/Parser/Executable/Validation/ExecutableValidator.ts create mode 100644 src/application/Parser/ProjectDetailsParser.ts create mode 100644 src/application/Parser/ScriptingDefinition/CodeSubstituter.ts create mode 100644 src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts create mode 100644 src/application/Repository/Repository.ts create mode 100644 src/application/Repository/RepositoryEntity.ts create mode 100644 src/application/ScriptDiagnostics/ScriptDiagnosticsCollector.ts create mode 100644 src/application/collections/.schema.yaml create mode 100644 src/application/collections/README.md create mode 100644 src/application/collections/collection.yaml.d.ts create mode 100644 src/application/collections/linux.yaml create mode 100644 src/application/collections/macos.yaml create mode 100644 src/application/collections/windows.yaml create mode 100644 src/domain/Application.ts create mode 100644 src/domain/Collection/CategoryCollection.ts create mode 100644 src/domain/Collection/ICategoryCollection.ts create mode 100644 src/domain/Collection/Validation/CategoryCollectionValidator.ts create mode 100644 src/domain/Collection/Validation/CompositeCategoryCollectionValidator.ts create mode 100644 src/domain/Collection/Validation/Rules/EnsureKnownOperatingSystem.ts create mode 100644 src/domain/Collection/Validation/Rules/EnsurePresenceOfAllRecommendationLevels.ts create mode 100644 src/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneCategory.ts create mode 100644 src/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneScript.ts create mode 100644 src/domain/Collection/Validation/Rules/EnsureUniqueIdsAcrossExecutables.ts create mode 100644 src/domain/Executables/Category/Category.ts create mode 100644 src/domain/Executables/Category/CategoryFactory.ts create mode 100644 src/domain/Executables/Documentable.ts create mode 100644 src/domain/Executables/Executable.ts create mode 100644 src/domain/Executables/Identifiable.ts create mode 100644 src/domain/Executables/Script/Code/DistinctReversibleScriptCode.ts create mode 100644 src/domain/Executables/Script/Code/ScriptCode.ts create mode 100644 src/domain/Executables/Script/Code/ScriptCodeFactory.ts create mode 100644 src/domain/Executables/Script/RecommendationLevel.ts create mode 100644 src/domain/Executables/Script/Script.ts create mode 100644 src/domain/Executables/Script/ScriptFactory.ts create mode 100644 src/domain/IApplication.ts create mode 100644 src/domain/IScriptingDefinition.ts create mode 100644 src/domain/OperatingSystem.ts create mode 100644 src/domain/Project/GitHubProjectDetails.ts create mode 100644 src/domain/Project/ProjectDetails.ts create mode 100644 src/domain/ScriptingDefinition.ts create mode 100644 src/domain/ScriptingLanguage.ts create mode 100644 src/domain/Version.ts create mode 100644 src/infrastructure/CodeRunner/Creation/Directory/PersistentDirectoryProvider.ts create mode 100644 src/infrastructure/CodeRunner/Creation/Directory/ScriptDirectoryProvider.ts create mode 100644 src/infrastructure/CodeRunner/Creation/Filename/FilenameGenerator.ts create mode 100644 src/infrastructure/CodeRunner/Creation/Filename/TimestampedFilenameGenerator.ts create mode 100644 src/infrastructure/CodeRunner/Creation/ScriptFileCreationOrchestrator.ts create mode 100644 src/infrastructure/CodeRunner/Creation/ScriptFileCreator.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/CommandDefinition.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/LinuxVisibleTerminalCommand.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/MacOsVisibleTerminalCommand.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PosixShellArgumentEscaper.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/ShellArgumentEscaper.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Factory/CommandDefinitionFactory.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Factory/OsSpecificTerminalLaunchCommandFactory.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/CommandDefinitionRunner.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/ExecutableFileShellCommandDefinitionRunner.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/PermissionSetter/ExecutablePermissionSetter.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/PermissionSetter/FileSystemExecutablePermissionSetter.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/ShellRunner/LoggingNodeShellCommandRunner.ts create mode 100644 src/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/ShellRunner/ShellCommandRunner.ts create mode 100644 src/infrastructure/CodeRunner/Execution/ScriptFileExecutor.ts create mode 100644 src/infrastructure/CodeRunner/Execution/VisibleTerminalFileRunner.ts create mode 100644 src/infrastructure/CodeRunner/ScriptFileCodeRunner.ts create mode 100644 src/infrastructure/CodeRunner/System/NodeElectronSystemOperations.ts create mode 100644 src/infrastructure/CodeRunner/System/SystemOperations.ts create mode 100644 src/infrastructure/Dialog/Browser/BrowserDialog.ts create mode 100644 src/infrastructure/Dialog/Browser/BrowserSaveFileDialog.ts create mode 100644 src/infrastructure/Dialog/Browser/FileSaverDialog.ts create mode 100644 src/infrastructure/Dialog/Electron/ElectronDialog.ts create mode 100644 src/infrastructure/Dialog/Electron/ElectronSaveFileDialog.ts create mode 100644 src/infrastructure/Dialog/Electron/NodeElectronSaveFileDialog.ts create mode 100644 src/infrastructure/Dialog/LoggingDialogDecorator.ts create mode 100644 src/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.ts create mode 100644 src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts create mode 100644 src/infrastructure/EnvironmentVariables/IAppMetadata.ts create mode 100644 src/infrastructure/EnvironmentVariables/IEnvironmentVariables.ts create mode 100644 src/infrastructure/EnvironmentVariables/IEnvironmentVariablesFactory.ts create mode 100644 src/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentKeys.ts create mode 100644 src/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables.ts create mode 100644 src/infrastructure/EnvironmentVariables/Vite/vite-env.d.ts create mode 100644 src/infrastructure/Events/EventSource.ts create mode 100644 src/infrastructure/Events/EventSubscriptionCollection.ts create mode 100644 src/infrastructure/Events/IEventSource.ts create mode 100644 src/infrastructure/Events/IEventSubscriptionCollection.ts create mode 100644 src/infrastructure/Log/ConsoleLogger.ts create mode 100644 src/infrastructure/Log/ElectronLogger.ts create mode 100644 src/infrastructure/Log/NoopLogger.ts create mode 100644 src/infrastructure/Log/WindowInjectedLogger.ts create mode 100644 src/infrastructure/ReadbackFileWriter/NodeReadbackFileWriter.ts create mode 100644 src/infrastructure/ReadbackFileWriter/ReadbackFileWriter.ts create mode 100644 src/infrastructure/Repository/InMemoryRepository.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Browser/BrowserOs/BrowserCondition.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Browser/BrowserOs/BrowserConditions.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Browser/BrowserOs/BrowserOsDetector.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Browser/BrowserOs/ConditionBasedOsDetector.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Browser/BrowserRuntimeEnvironment.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Browser/TouchSupportDetection.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Electron/ContextIsolatedElectronDetector.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Node/NodeOsMapper.ts create mode 100644 src/infrastructure/RuntimeEnvironment/Node/NodeRuntimeEnvironment.ts create mode 100644 src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts create mode 100644 src/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.ts create mode 100644 src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts create mode 100644 src/infrastructure/RuntimeSanity/Common/ISanityCheckOptions.ts create mode 100644 src/infrastructure/RuntimeSanity/Common/ISanityValidator.ts create mode 100644 src/infrastructure/RuntimeSanity/SanityChecks.ts create mode 100644 src/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.ts create mode 100644 src/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.ts create mode 100644 src/infrastructure/ScriptDiagnostics/ScriptEnvironmentDiagnosticsCollector.ts create mode 100644 src/infrastructure/Threading/AsyncLazy.ts create mode 100644 src/infrastructure/Threading/AsyncSleep.ts create mode 100644 src/infrastructure/WindowVariables/WindowVariables.ts create mode 100644 src/infrastructure/WindowVariables/WindowVariablesValidator.ts create mode 100644 src/infrastructure/WindowVariables/window-variables.d.ts create mode 100644 src/presentation/README.md create mode 100644 src/presentation/assets/fonts/roboto-mono-v23-latin-regular.woff2 create mode 100644 src/presentation/assets/fonts/roboto-slab-v34-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-700.ttf create mode 100644 src/presentation/assets/fonts/roboto-slab-v34-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-700.woff2 create mode 100644 src/presentation/assets/fonts/roboto-slab-v34-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.ttf create mode 100644 src/presentation/assets/fonts/roboto-slab-v34-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2 create mode 100644 src/presentation/assets/fonts/slabo-27px-v14-latin_latin-ext-regular.ttf create mode 100644 src/presentation/assets/fonts/slabo-27px-v14-latin_latin-ext-regular.woff2 create mode 100644 src/presentation/assets/fonts/source-code-pro-v23-latin-regular.ttf create mode 100644 src/presentation/assets/fonts/source-code-pro-v23-latin-regular.woff2 create mode 100644 src/presentation/assets/fonts/yesteryear-v18-latin-regular.ttf create mode 100644 src/presentation/assets/fonts/yesteryear-v18-latin-regular.woff2 create mode 100644 src/presentation/assets/icons/battery-full.svg create mode 100644 src/presentation/assets/icons/battery-half.svg create mode 100644 src/presentation/assets/icons/circle-info.svg create mode 100644 src/presentation/assets/icons/copy.svg create mode 100644 src/presentation/assets/icons/desktop.svg create mode 100644 src/presentation/assets/icons/external-link.svg create mode 100644 src/presentation/assets/icons/face-smile.svg create mode 100644 src/presentation/assets/icons/file-arrow-down.svg create mode 100644 src/presentation/assets/icons/floppy-disk.svg create mode 100644 src/presentation/assets/icons/folder-open.svg create mode 100644 src/presentation/assets/icons/folder.svg create mode 100644 src/presentation/assets/icons/github.svg create mode 100644 src/presentation/assets/icons/globe.svg create mode 100644 src/presentation/assets/icons/left-right.svg create mode 100644 src/presentation/assets/icons/lightbulb.svg create mode 100644 src/presentation/assets/icons/magnifying-glass.svg create mode 100644 src/presentation/assets/icons/play.svg create mode 100644 src/presentation/assets/icons/rotate-left.svg create mode 100644 src/presentation/assets/icons/shield.svg create mode 100644 src/presentation/assets/icons/square-check.svg create mode 100644 src/presentation/assets/icons/tag.svg create mode 100644 src/presentation/assets/icons/triangle-exclamation.svg create mode 100644 src/presentation/assets/icons/user-secret.svg create mode 100644 src/presentation/assets/icons/xmark.svg create mode 100644 src/presentation/assets/styles/_colors.scss create mode 100644 src/presentation/assets/styles/_fonts.scss create mode 100644 src/presentation/assets/styles/_media.scss create mode 100644 src/presentation/assets/styles/_mixins.scss create mode 100644 src/presentation/assets/styles/_spacing.scss create mode 100644 src/presentation/assets/styles/_typography.scss create mode 100644 src/presentation/assets/styles/_vite-path.scss create mode 100644 src/presentation/assets/styles/base/_code-styling.scss create mode 100644 src/presentation/assets/styles/base/_index.scss create mode 100644 src/presentation/assets/styles/base/_link-styling.scss create mode 100644 src/presentation/assets/styles/base/_margin-padding.scss create mode 100644 src/presentation/assets/styles/base/_prevent-scrollbar-layout-shift.scss create mode 100644 src/presentation/assets/styles/main.scss create mode 100644 src/presentation/bootstrapping/ApplicationBootstrapper.ts create mode 100644 src/presentation/bootstrapping/Bootstrapper.ts create mode 100644 src/presentation/bootstrapping/DependencyProvider.ts create mode 100644 src/presentation/bootstrapping/Modules/AppInitializationLogger.ts create mode 100644 src/presentation/bootstrapping/Modules/DependencyBootstrapper.ts create mode 100644 src/presentation/bootstrapping/Modules/MobileSafariActivePseudoClassEnabler.ts create mode 100644 src/presentation/bootstrapping/Modules/RuntimeSanityValidator.ts create mode 100644 src/presentation/common/Dialog.ts create mode 100644 src/presentation/components/App.vue create mode 100644 src/presentation/components/Code/CodeButtons/CodeCopyButton.vue create mode 100644 src/presentation/components/Code/CodeButtons/CodeRunButton.vue create mode 100644 src/presentation/components/Code/CodeButtons/IconButton.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/CodeSaveButton.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/Help/InfoTooltipInline.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/Help/InfoTooltipWrapper.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/RunInstructions.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/InstructionStep.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/InstructionSteps.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/PlatformInstructionSteps.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue create mode 100644 src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue create mode 100644 src/presentation/components/Code/CodeButtons/ScriptErrorDialog.ts create mode 100644 src/presentation/components/Code/CodeButtons/TheCodeButtons.vue create mode 100644 src/presentation/components/Code/TheCodeArea.vue create mode 100644 src/presentation/components/Code/ace-importer.ts create mode 100644 src/presentation/components/DevToolkit/DevToolkit.vue create mode 100644 src/presentation/components/DevToolkit/DumpNames.ts create mode 100644 src/presentation/components/DevToolkit/UseScrollbarGutterWidth.ts create mode 100644 src/presentation/components/Scripts/Menu/MenuOptionList.vue create mode 100644 src/presentation/components/Scripts/Menu/MenuOptionListItem.vue create mode 100644 src/presentation/components/Scripts/Menu/Recommendation/Rating/CircleRating.vue create mode 100644 src/presentation/components/Scripts/Menu/Recommendation/Rating/RatingCircle.vue create mode 100644 src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue create mode 100644 src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusHandler.ts create mode 100644 src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusType.ts create mode 100644 src/presentation/components/Scripts/Menu/Recommendation/TheRecommendationSelector.vue create mode 100644 src/presentation/components/Scripts/Menu/Revert/RevertStatusDocumentation.vue create mode 100644 src/presentation/components/Scripts/Menu/Revert/RevertStatusHandler.ts create mode 100644 src/presentation/components/Scripts/Menu/Revert/RevertStatusType.ts create mode 100644 src/presentation/components/Scripts/Menu/Revert/TheRevertSelector.vue create mode 100644 src/presentation/components/Scripts/Menu/TheOsChanger.vue create mode 100644 src/presentation/components/Scripts/Menu/TheScriptsMenu.vue create mode 100644 src/presentation/components/Scripts/Menu/View/TheViewChanger.vue create mode 100644 src/presentation/components/Scripts/Menu/View/ViewType.ts create mode 100644 src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue create mode 100644 src/presentation/components/Scripts/Slider/SliderHandle.vue create mode 100644 src/presentation/components/Scripts/Slider/UseDragHandler.ts create mode 100644 src/presentation/components/Scripts/Slider/UseGlobalCursor.ts create mode 100644 src/presentation/components/Scripts/TheScriptArea.vue create mode 100644 src/presentation/components/Scripts/View/Cards/CardExpandTransition.vue create mode 100644 src/presentation/components/Scripts/View/Cards/CardExpansionArrow.vue create mode 100644 src/presentation/components/Scripts/View/Cards/CardList.vue create mode 100644 src/presentation/components/Scripts/View/Cards/CardListItem.vue create mode 100644 src/presentation/components/Scripts/View/Cards/CardSelectionIndicator.vue create mode 100644 src/presentation/components/Scripts/View/Cards/NonCollapsingDirective.ts create mode 100644 src/presentation/components/Scripts/View/Cards/card-gap.scss create mode 100644 src/presentation/components/Scripts/View/TheScriptsView.vue create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentableNode.vue create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentationText.vue create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/ToggleDocumentationButton.vue create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/MarkdownRenderer.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/MarkdownText.vue create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/InlineReferenceLabelsToSuperscriptConverter.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/MarkdownItHtmlRenderer.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/PlainTextUrlsToHyperlinksConverter.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/NodeContent.vue create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/NodeTitle.vue create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/RevertToggle.vue create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/Reverter.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts create mode 100644 src/presentation/components/Scripts/View/Tree/NodeContent/ToggleSwitch.vue create mode 100644 src/presentation/components/Scripts/View/Tree/ScriptsTree.vue create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeNodeStateChangedEmittedEvent.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/HierarchicalTreeNode.vue create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/HierarchyAccess.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/InteractableNode.vue create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/LeafTreeNode.vue create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/NodeCheckbox.vue create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeState.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeStateTransactionDescriber.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNodeManager.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Node/UseNodeState.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Rendering/DelayScheduler.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/RenderQueueOrderer.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/NodeRenderingStrategy.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/TimeoutDelayScheduler.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeCollectionFocusManager.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeFocusManager.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/TreeNodeNavigator.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeCollection.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeInitializerAndUpdater.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot.vue create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRootManager.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateChildrenCheckState.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateParentCheckState.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/UseLeafNodeCheckedStateUpdater.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/UseTreeKeyboardNavigation.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/UseTreeQueryFilter.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/tree-colors.scss create mode 100644 src/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeViewAdapter/TreeNodeMetadataConverter.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseCollectionSelectionStateUpdater.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseTreeViewFilterEvent.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseTreeViewNodeInput.ts create mode 100644 src/presentation/components/Shared/ExpandCollapse/ExpandCollapseTransition.vue create mode 100644 src/presentation/components/Shared/ExpandCollapse/UseExpandCollapseAnimation.ts create mode 100644 src/presentation/components/Shared/FlatButton.vue create mode 100644 src/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard.ts create mode 100644 src/presentation/components/Shared/Hooks/Clipboard/Clipboard.ts create mode 100644 src/presentation/components/Shared/Hooks/Clipboard/UseClipboard.ts create mode 100644 src/presentation/components/Shared/Hooks/Common/LifecycleHook.ts create mode 100644 src/presentation/components/Shared/Hooks/Dialog/ClientDialogFactory.ts create mode 100644 src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts create mode 100644 src/presentation/components/Shared/Hooks/Log/ClientLoggerFactory.ts create mode 100644 src/presentation/components/Shared/Hooks/Log/LoggerFactory.ts create mode 100644 src/presentation/components/Shared/Hooks/Log/UseLogger.ts create mode 100644 src/presentation/components/Shared/Hooks/README.md create mode 100644 src/presentation/components/Shared/Hooks/Resize/UseAnimationFrameLimiter.ts create mode 100644 src/presentation/components/Shared/Hooks/Resize/UseResizeObserver.ts create mode 100644 src/presentation/components/Shared/Hooks/Resize/UseResizeObserverPolyfill.ts create mode 100644 src/presentation/components/Shared/Hooks/UseApplication.ts create mode 100644 src/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.ts create mode 100644 src/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents.ts create mode 100644 src/presentation/components/Shared/Hooks/UseCodeRunner.ts create mode 100644 src/presentation/components/Shared/Hooks/UseCollectionState.ts create mode 100644 src/presentation/components/Shared/Hooks/UseCurrentCode.ts create mode 100644 src/presentation/components/Shared/Hooks/UseRuntimeEnvironment.ts create mode 100644 src/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector.ts create mode 100644 src/presentation/components/Shared/Hooks/UseUserSelectionState.ts create mode 100644 src/presentation/components/Shared/Icon/AppIcon.vue create mode 100644 src/presentation/components/Shared/Icon/IconName.ts create mode 100644 src/presentation/components/Shared/Icon/UseSvgLoader.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/ScrollLock/ScrollDomStateAccessor.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/ScrollLock/UseLockBodyBackgroundScroll.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/ScrollLock/WindowScrollDomStateAccessor.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.ts create mode 100644 src/presentation/components/Shared/Modal/ModalContainer.vue create mode 100644 src/presentation/components/Shared/Modal/ModalContent.vue create mode 100644 src/presentation/components/Shared/Modal/ModalDialog.vue create mode 100644 src/presentation/components/Shared/Modal/ModalOverlay.vue create mode 100644 src/presentation/components/Shared/OperatingSystemNames.ts create mode 100644 src/presentation/components/Shared/SizeObserver.vue create mode 100644 src/presentation/components/Shared/TooltipWrapper.vue create mode 100644 src/presentation/components/TheFooter/DownloadUrlList.vue create mode 100644 src/presentation/components/TheFooter/DownloadUrlListItem.vue create mode 100644 src/presentation/components/TheFooter/PrivacyPolicy.vue create mode 100644 src/presentation/components/TheFooter/TheFooter.vue create mode 100644 src/presentation/components/TheHeader.vue create mode 100644 src/presentation/components/TheSearchBar.vue create mode 100644 src/presentation/electron/build/README.md create mode 100644 src/presentation/electron/build/icon.ico create mode 100644 src/presentation/electron/build/icon.png create mode 100644 src/presentation/electron/main/ElectronConfig.ts create mode 100644 src/presentation/electron/main/IpcRegistration.ts create mode 100644 src/presentation/electron/main/Update/AutomaticUpdateCoordinator.ts create mode 100644 src/presentation/electron/main/Update/ElectronAutoUpdaterFactory.ts create mode 100644 src/presentation/electron/main/Update/ManualUpdater/Dialogs.ts create mode 100644 src/presentation/electron/main/Update/ManualUpdater/Downloader.ts create mode 100644 src/presentation/electron/main/Update/ManualUpdater/Installer.ts create mode 100644 src/presentation/electron/main/Update/ManualUpdater/Integrity.ts create mode 100644 src/presentation/electron/main/Update/ManualUpdater/ManualUpdateCoordinator.ts create mode 100644 src/presentation/electron/main/Update/ManualUpdater/RetryFileSystemAccess.ts create mode 100644 src/presentation/electron/main/Update/UpdateInitializer.ts create mode 100644 src/presentation/electron/main/Update/UpdateProgressBar.ts create mode 100644 src/presentation/electron/main/index.ts create mode 100644 src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts create mode 100644 src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts create mode 100644 src/presentation/electron/preload/ContextBridging/README.md create mode 100644 src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts create mode 100644 src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts create mode 100644 src/presentation/electron/preload/index.ts create mode 100644 src/presentation/electron/shared/IpcBridging/IpcChannel.ts create mode 100644 src/presentation/electron/shared/IpcBridging/IpcChannelDefinitions.ts create mode 100644 src/presentation/electron/shared/IpcBridging/IpcProxy.ts create mode 100644 src/presentation/electron/shared/IpcBridging/README.md create mode 100644 src/presentation/index.html create mode 100644 src/presentation/injectionSymbols.ts create mode 100644 src/presentation/main.ts create mode 100644 src/presentation/public/favicon.ico create mode 100644 src/presentation/public/icon.png create mode 100644 src/presentation/public/robots.txt create mode 100644 tests/.eslintrc.cjs create mode 100644 tests/README.md create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/README.md create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/app-logs.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/check-for-errors.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/error-ignore-patterns.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/common/app-artifact-locator.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/common/extraction-result.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/linux.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/macos.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/windows.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/runner.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/system-capture/screen-capture.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/system-capture/window-title-capture.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/cli-args.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/config.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/index.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/main.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/io.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/log.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/npm.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/platform.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/run-command.ts create mode 100644 tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/sleep.ts create mode 100644 tests/checks/desktop-runtime-errors/main.spec.ts create mode 100644 tests/checks/external-urls/DocumentationUrlExtractor.ts create mode 100644 tests/checks/external-urls/StatusChecker/BatchStatusChecker.ts create mode 100644 tests/checks/external-urls/StatusChecker/ExponentialBackOffRetryHandler.ts create mode 100644 tests/checks/external-urls/StatusChecker/FetchFollow.ts create mode 100644 tests/checks/external-urls/StatusChecker/FetchWithTimeout.ts create mode 100644 tests/checks/external-urls/StatusChecker/README.md create mode 100644 tests/checks/external-urls/StatusChecker/Requestor.ts create mode 100644 tests/checks/external-urls/StatusChecker/TlsFingerprintRandomizer.ts create mode 100644 tests/checks/external-urls/StatusChecker/UrlDomainProcessing.ts create mode 100644 tests/checks/external-urls/StatusChecker/UrlStatus.ts create mode 100644 tests/checks/external-urls/StatusChecker/UserAgents.ts create mode 100644 tests/checks/external-urls/TestExecutionDetailsLogger.ts create mode 100644 tests/checks/external-urls/main.spec.ts create mode 100644 tests/e2e/.eslintrc.cjs create mode 100644 tests/e2e/.gitignore create mode 100644 tests/e2e/card-list-layout-stability-on-load.cy.ts create mode 100644 tests/e2e/code-highlighting.cy.ts create mode 100644 tests/e2e/initialization.cy.ts create mode 100644 tests/e2e/no-unintended-layout-shifts.cy.ts create mode 100644 tests/e2e/no-unintended-overflow.cy.ts create mode 100644 tests/e2e/operating-system-selector.cy.ts create mode 100644 tests/e2e/revert-toggle.cy.ts create mode 100644 tests/e2e/support/assert/layout-stability.ts create mode 100644 tests/e2e/support/commands.ts create mode 100644 tests/e2e/support/e2e.ts create mode 100644 tests/e2e/support/interactions/card.ts create mode 100644 tests/e2e/support/interactions/code-area.ts create mode 100644 tests/e2e/support/interactions/header.ts create mode 100644 tests/e2e/support/interactions/script-selection.ts create mode 100644 tests/e2e/support/scenarios/viewport-test-scenarios.ts create mode 100644 tests/e2e/tsconfig.json create mode 100644 tests/integration/application/Parser/ApplicationParser.spec.ts create mode 100644 tests/integration/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.spec.ts create mode 100644 tests/integration/composite/DependencyResolution.spec.ts create mode 100644 tests/integration/composite/README.md create mode 100644 tests/integration/infrastructure/CodeRunner/ScriptFileCodeRunner.spec.ts create mode 100644 tests/integration/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.spec.ts create mode 100644 tests/integration/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables.spec.ts create mode 100644 tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsTestCases.ts create mode 100644 tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/ConditionBasedOsDetector.spec.ts create mode 100644 tests/integration/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.spec.ts create mode 100644 tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts create mode 100644 tests/integration/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.spec.ts create mode 100644 tests/integration/infrastructure/RuntimeSanity/Validators/ValidatorTestRunner.ts create mode 100644 tests/integration/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.spec.ts create mode 100644 tests/integration/presentation/bootstrapping/ApplicationBootstrapper.spec.ts create mode 100644 tests/integration/presentation/bootstrapping/Modules/MobileSafariActivePseudoClassEnabler.spec.ts create mode 100644 tests/integration/presentation/bootstrapping/Modules/MobileSafariDetectionTestCases.ts create mode 100644 tests/integration/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer.spec.ts create mode 100644 tests/integration/presentation/components/Scripts/View/Tree/Shared/Icon/UseSvgLoader.spec.ts create mode 100644 tests/integration/presentation/components/Scripts/View/Tree/Shared/Modal/Hooks/UseEscapeKeyListener.spec.ts create mode 100644 tests/integration/presentation/components/Scripts/View/Tree/Shared/OperatingSystemNames.spec.ts create mode 100644 tests/integration/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState.spec.ts create mode 100644 tests/integration/presentation/components/Scripts/View/Tree/TreeView/TreeView.spec.ts create mode 100644 tests/integration/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.spec.ts create mode 100644 tests/integration/presentation/components/Shared/Modal/ModalContainer.spec.ts create mode 100644 tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts create mode 100644 tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts create mode 100644 tests/integration/shared/TestCases/TouchSupportOptions.ts create mode 100644 tests/shared/Assertions/ExpectDeepIncludes.ts create mode 100644 tests/shared/Assertions/ExpectDeepThrowsError.ts create mode 100644 tests/shared/Assertions/ExpectExists.ts create mode 100644 tests/shared/Assertions/ExpectThrowsAsync.ts create mode 100644 tests/shared/Assertions/ExpectTrue.ts create mode 100644 tests/shared/FormatAssertionMessage.ts create mode 100644 tests/shared/HtmlParser.ts create mode 100644 tests/shared/Spies/EventTargetSpy.ts create mode 100644 tests/shared/TestCases/SupportedOperatingSystems.ts create mode 100644 tests/shared/Vue/ExecuteInComponentSetupContext.ts create mode 100644 tests/shared/Vue/WaitForValueChange.ts create mode 100644 tests/shared/bootstrap/BlobPolyfill.ts create mode 100644 tests/shared/bootstrap/FailTestOnConsoleError.ts create mode 100644 tests/shared/bootstrap/setup.ts create mode 100644 tests/unit/application/ApplicationFactory.spec.ts create mode 100644 tests/unit/application/Common/Array.ComparerTestScenario.ts create mode 100644 tests/unit/application/Common/Array.spec.ts create mode 100644 tests/unit/application/Common/CustomError.spec.ts create mode 100644 tests/unit/application/Common/Enum.spec.ts create mode 100644 tests/unit/application/Common/EnumRangeTestRunner.ts create mode 100644 tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactory.spec.ts create mode 100644 tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner.ts create mode 100644 tests/unit/application/Common/Shuffle.spec.ts create mode 100644 tests/unit/application/Common/Text/FilterEmptyStrings.spec.ts create mode 100644 tests/unit/application/Common/Text/IndentText.spec.ts create mode 100644 tests/unit/application/Common/Text/SplitTextIntoLines.spec.ts create mode 100644 tests/unit/application/Common/Timing/BatchedDebounce.spec.ts create mode 100644 tests/unit/application/Common/Timing/PlatformTimer.spec.ts create mode 100644 tests/unit/application/Common/Timing/Throttle.spec.ts create mode 100644 tests/unit/application/Context/ApplicationContext.spec.ts create mode 100644 tests/unit/application/Context/ApplicationContextFactory.spec.ts create mode 100644 tests/unit/application/Context/State/CategoryCollectionState.spec.ts create mode 100644 tests/unit/application/Context/State/Code/ApplicationCode.spec.ts create mode 100644 tests/unit/application/Context/State/Code/Event/CodeChangedEvent.spec.ts create mode 100644 tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts create mode 100644 tests/unit/application/Context/State/Code/Generation/CodeBuilderFactory.spec.ts create mode 100644 tests/unit/application/Context/State/Code/Generation/Languages/BatchBuilder.spec.ts create mode 100644 tests/unit/application/Context/State/Code/Generation/Languages/ShellBuilder.spec.ts create mode 100644 tests/unit/application/Context/State/Code/Generation/UserScriptGenerator.spec.ts create mode 100644 tests/unit/application/Context/State/Code/Position/CodePosition.spec.ts create mode 100644 tests/unit/application/Context/State/Filter/AdaptiveFilterContext.spec.ts create mode 100644 tests/unit/application/Context/State/Filter/Event/FilterChange.spec.ts create mode 100644 tests/unit/application/Context/State/Filter/Result/AppliedFilterResult.spec.ts create mode 100644 tests/unit/application/Context/State/Filter/Strategy/LinearFilterStrategy.spec.ts create mode 100644 tests/unit/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.spec.ts create mode 100644 tests/unit/application/Context/State/Selection/Script/DebouncedScriptSelection.spec.ts create mode 100644 tests/unit/application/Context/State/Selection/Script/ExpectEqualSelectedScripts.ts create mode 100644 tests/unit/application/Context/State/Selection/Script/UserSelectedScript.spec.ts create mode 100644 tests/unit/application/Context/State/Selection/UserSelectionFacade.spec.ts create mode 100644 tests/unit/application/Parser/ApplicationParser.spec.ts create mode 100644 tests/unit/application/Parser/CategoryCollectionParser.spec.ts create mode 100644 tests/unit/application/Parser/Common/ContextualError.spec.ts create mode 100644 tests/unit/application/Parser/Common/ContextualErrorTester.ts create mode 100644 tests/unit/application/Parser/Common/TypeValidator.spec.ts create mode 100644 tests/unit/application/Parser/Executable/CategoryCollectionSpecificUtilities.spec.ts create mode 100644 tests/unit/application/Parser/Executable/CategoryParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/DocumentationParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/Expression.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/EscapeDoubleQuotes.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CommonInlinePowerShellTestUtilities.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateAbsentCodeTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateCommentedCodeTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateDoUntilTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateDoWhileTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateForLoopTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateForeachTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateFunctionTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateHereStringTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateIfStatementTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateLineContinuationBacktickTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateNewlineTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateScriptBlockTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateSwitchTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateTryCatchFinallyTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateWhileTests.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/PipeTestRunner.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeFactory.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipelineCompiler.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/SyntaxParserTestsRunner.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/NestedFunctionCallCompiler.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/AdaptiveFunctionCallCompiler.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/ExpectFunctionBodyType.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunction.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionCollection.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameValidator.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompiler.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/ScriptParser.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Validation/CodeValidator.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Validation/Rules/CodeValidationRuleTestRunner.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Validation/Syntax/ConcreteSyntaxes.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Script/Validation/Syntax/SyntaxFactory.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Validation/DataValidationTestScenarioGenerator.ts create mode 100644 tests/unit/application/Parser/Executable/Validation/ExecutableErrorContextMessage.spec.ts create mode 100644 tests/unit/application/Parser/Executable/Validation/ExecutableValidationTester.ts create mode 100644 tests/unit/application/Parser/Executable/Validation/ExecutableValidator.spec.ts create mode 100644 tests/unit/application/Parser/ProjectDetailsParser.spec.ts create mode 100644 tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts create mode 100644 tests/unit/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.spec.ts create mode 100644 tests/unit/application/collections/NoUnintentedInlining.spec.ts create mode 100644 tests/unit/application/collections/raw-loader.d.ts create mode 100644 tests/unit/domain/Application.spec.ts create mode 100644 tests/unit/domain/Collection/CategoryCollection.spec.ts create mode 100644 tests/unit/domain/Collection/Validation/CompositeCategoryCollectionValidator.spec.ts create mode 100644 tests/unit/domain/Collection/Validation/Rules/EnsureKnownOperatingSystem.spec.ts create mode 100644 tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAllRecommendationLevels.spec.ts create mode 100644 tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneCategory.spec.ts create mode 100644 tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneScript.spec.ts create mode 100644 tests/unit/domain/Collection/Validation/Rules/EnsureUniqueIdsAcrossExecutables.spec.ts create mode 100644 tests/unit/domain/Executables/Category/CategoryFactory.spec.ts create mode 100644 tests/unit/domain/Executables/Script/Code/ScriptCode.spec.ts create mode 100644 tests/unit/domain/Executables/Script/ScriptFactory.spec.ts create mode 100644 tests/unit/domain/Project/GitHubProjectDetails.spec.ts create mode 100644 tests/unit/domain/ScriptCodeFactory.spec.ts create mode 100644 tests/unit/domain/ScriptingDefinition.spec.ts create mode 100644 tests/unit/domain/Version.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Creation/Directory/PersistentDirectoryProvider.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Creation/Filename/TimestampedFilenameGenerator.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Creation/ScriptFileCreationOrchestrator.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/LinuxVisibleTerminalCommand.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/MacOsVisibleTerminalCommand.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PosixShellArgumentEscaper.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/ShellArgumentEscaperTestRunner.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Factory/OsSpecificTerminalLaunchCommandFactory.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/ExecutableFileShellCommandDefinitionRunner.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/PermissionSetter/FileSystemExecutablePermissionSetter.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/ShellRunner/LoggingNodeShellCommandRunner.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/Execution/VisibleTerminalFileRunner.spec.ts create mode 100644 tests/unit/infrastructure/CodeRunner/ScriptFileCodeRunner.spec.ts create mode 100644 tests/unit/infrastructure/Dialog/Browser/BrowserDialog.spec.ts create mode 100644 tests/unit/infrastructure/Dialog/Browser/FileSaverDialog.spec.ts create mode 100644 tests/unit/infrastructure/Dialog/Electron/ElectronDialog.spec.ts create mode 100644 tests/unit/infrastructure/Dialog/Electron/ElectronFileDialogOperationsStub.ts create mode 100644 tests/unit/infrastructure/Dialog/Electron/NodeElectronSaveFileDialog.spec.ts create mode 100644 tests/unit/infrastructure/Dialog/Electron/NodePathOperationsStub.ts create mode 100644 tests/unit/infrastructure/Dialog/LoggingDialogDecorator.spec.ts create mode 100644 tests/unit/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.ts create mode 100644 tests/unit/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.spec.ts create mode 100644 tests/unit/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentKeys.spec.ts create mode 100644 tests/unit/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables.spec.ts create mode 100644 tests/unit/infrastructure/Events/EventSource.spec.ts create mode 100644 tests/unit/infrastructure/Events/EventSubscriptionCollection.spec.ts create mode 100644 tests/unit/infrastructure/InMemoryRepository.spec.ts create mode 100644 tests/unit/infrastructure/Log/ConsoleLogger.spec.ts create mode 100644 tests/unit/infrastructure/Log/ElectronLogger.spec.ts create mode 100644 tests/unit/infrastructure/Log/LoggerTestRunner.ts create mode 100644 tests/unit/infrastructure/Log/NoopLogger.spec.ts create mode 100644 tests/unit/infrastructure/Log/WindowInjectedLogger.spec.ts create mode 100644 tests/unit/infrastructure/ReadbackFileWriter/FileReadWriteOperationsStub.ts create mode 100644 tests/unit/infrastructure/ReadbackFileWriter/NodeReadbackFileWriter.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeEnvironment/Browser/BrowserOs/ConditionBasedOsDetector.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeEnvironment/Browser/BrowserRuntimeEnvironment.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeEnvironment/Browser/TouchSupportDetection.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeEnvironment/Electron/ContextIsolatedElectronDetector.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeEnvironment/Node/NodeOsMapper.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeEnvironment/Node/NodeRuntimeEnvironment.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeEnvironment/WindowVariablesValidator.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeSanity/Common/FactoryValidator.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeSanity/SanityChecks.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeSanity/Validators/FactoryValidatorConcreteTestRunner.ts create mode 100644 tests/unit/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.spec.ts create mode 100644 tests/unit/infrastructure/ScriptDiagnostics/ScriptEnvironmentDiagnosticsCollector.spec.ts create mode 100644 tests/unit/infrastructure/Threading/AsyncLazy.spec.ts create mode 100644 tests/unit/infrastructure/Threading/AsyncSleep.spec.ts create mode 100644 tests/unit/presentation/bootstrapping/ApplicationBootstrapper.spec.ts create mode 100644 tests/unit/presentation/bootstrapping/DependencyProvider.spec.ts create mode 100644 tests/unit/presentation/bootstrapping/Modules/AppInitializationLogger.spec.ts create mode 100644 tests/unit/presentation/bootstrapping/Modules/DependencyBootstrapper.spec.ts create mode 100644 tests/unit/presentation/bootstrapping/Modules/MobileSafariActivePseudoClassEnabler.spec.ts create mode 100644 tests/unit/presentation/bootstrapping/Modules/RuntimeSanityValidator.spec.ts create mode 100644 tests/unit/presentation/components/App.spec.ts create mode 100644 tests/unit/presentation/components/Code/CodeButtons/CodeCopyButton.spec.ts create mode 100644 tests/unit/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.spec.ts create mode 100644 tests/unit/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/PlatformInstructionSteps.spec.ts create mode 100644 tests/unit/presentation/components/Code/CodeButtons/ScriptErrorDialog.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/Menu/MenuOptionList.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/Menu/Recommendation/Rating/CircleRating.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/Menu/Recommendation/Rating/RatingCircle.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusHandler.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusTestScenario.ts create mode 100644 tests/unit/presentation/components/Scripts/Menu/Revert/RevertStatusHandler.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/Slider/UseDragHandler.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/Slider/UseGlobalCursor.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Cards/CardList.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Cards/NonCollapsingDirective.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/TheScriptsView.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Modifiers/InlineReferenceLabelsToSuperscriptConverter.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Modifiers/MarkdownItHtmlRenderer.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Modifiers/MarkdownRenderingTester.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Modifiers/PlainTextUrlsToHyperlinksConverter.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/NodeContent/ToggleSwitch.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeViewFilterEvent.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeState.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeStateTransactionDescriber.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNodeManager.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Node/UseNodeState.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/TimeoutDelayScheduler.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeCollectionFocusManager.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/TreeNodeNavigator.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeInitializerAndUpdater.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRootManager.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateChildrenCheckState.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateParentCheckState.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeViewAdapter/TreeNodeMetadataConverter.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseCollectionSelectionStateUpdater.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseTreeViewFilterEvent.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseTreeViewNodeInput.spec.ts create mode 100644 tests/unit/presentation/components/Shared/ExpandCollapse/UseExpandCollapseAnimation.spec.ts create mode 100644 tests/unit/presentation/components/Shared/FlatButton.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/Clipboard/UseClipboard.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/Dialog/ClientDialogFactory.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/Dialog/UseDialog.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/Log/ClientLoggerFactory.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/Log/UseLogger.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseApplication.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseCodeRunner.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseCollectionState.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseCurrentCode.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseRuntimeEnvironment.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseUserSelectionState.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Icon/AppIcon.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Icon/UseSvgLoader.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/Hooks/ScrollLock/DomStateChangeTestScenarios.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/Hooks/ScrollLock/UseLockBodyBackgroundScroll.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/ModalContainer.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/ModalContent.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/ModalDialog.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/ModalOverlay.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Resize/UseAnimationFrameLimiter.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Resize/UseResizeObserver.spec.ts create mode 100644 tests/unit/presentation/electron/main/IpcRegistration.spec.ts create mode 100644 tests/unit/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts create mode 100644 tests/unit/presentation/electron/preload/ContextBridging/MethodContextBinder.spec.ts create mode 100644 tests/unit/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts create mode 100644 tests/unit/presentation/electron/preload/ContextBridging/SecureFacadeCreator.spec.ts create mode 100644 tests/unit/presentation/electron/shared/IpcChannelDefinitions.spec.ts create mode 100644 tests/unit/presentation/electron/shared/IpcProxy.spec.ts create mode 100644 tests/unit/presentation/injectionSymbols.ts create mode 100644 tests/unit/shared/ExceptionCollector.ts create mode 100644 tests/unit/shared/PromiseInspection.ts create mode 100644 tests/unit/shared/Stubs/AppMetadataStub.ts create mode 100644 tests/unit/shared/Stubs/ApplicationCodeStub.ts create mode 100644 tests/unit/shared/Stubs/ApplicationContextChangedEventStub.ts create mode 100644 tests/unit/shared/Stubs/ApplicationContextStub.ts create mode 100644 tests/unit/shared/Stubs/ApplicationStub.ts create mode 100644 tests/unit/shared/Stubs/ArgumentCompilerStub.ts create mode 100644 tests/unit/shared/Stubs/BatchedDebounceStub.ts create mode 100644 tests/unit/shared/Stubs/BootstrapperStub.ts create mode 100644 tests/unit/shared/Stubs/BrowserConditionStub.ts create mode 100644 tests/unit/shared/Stubs/BrowserEnvironmentStub.ts create mode 100644 tests/unit/shared/Stubs/BrowserOsDetectorStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryCollectionFactoryStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryCollectionParserStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryCollectionStateStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryCollectionStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryCollectionValidationContextStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryDataStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryFactoryStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryParserStub.ts create mode 100644 tests/unit/shared/Stubs/CategorySelectionStub.ts create mode 100644 tests/unit/shared/Stubs/CategoryStub.ts create mode 100644 tests/unit/shared/Stubs/ChildProcesssStub.ts create mode 100644 tests/unit/shared/Stubs/ClipboardStub.ts create mode 100644 tests/unit/shared/Stubs/CodeChangedEventStub.ts create mode 100644 tests/unit/shared/Stubs/CodeRunnerStub.ts create mode 100644 tests/unit/shared/Stubs/CodeSegmentMergerStub.ts create mode 100644 tests/unit/shared/Stubs/CodeSubstituterStub.ts create mode 100644 tests/unit/shared/Stubs/CodeValidationRuleStub.ts create mode 100644 tests/unit/shared/Stubs/CodeValidatorStub.ts create mode 100644 tests/unit/shared/Stubs/CollectionDataStub.ts create mode 100644 tests/unit/shared/Stubs/CommandDefinitionFactoryStub.ts create mode 100644 tests/unit/shared/Stubs/CommandDefinitionRunnerStub.ts create mode 100644 tests/unit/shared/Stubs/CommandDefinitionStub.ts create mode 100644 tests/unit/shared/Stubs/CommandOpsStub.ts create mode 100644 tests/unit/shared/Stubs/CompiledCodeStub.ts create mode 100644 tests/unit/shared/Stubs/DelaySchedulerStub.ts create mode 100644 tests/unit/shared/Stubs/DialogStub.ts create mode 100644 tests/unit/shared/Stubs/ElectronEnvironmentDetectorStub.ts create mode 100644 tests/unit/shared/Stubs/EnumParserStub.ts create mode 100644 tests/unit/shared/Stubs/EnvironmentVariablesStub.ts create mode 100644 tests/unit/shared/Stubs/ErrorWithContextWrapperStub.ts create mode 100644 tests/unit/shared/Stubs/ErrorWrapperStub.ts create mode 100644 tests/unit/shared/Stubs/EventSourceStub.ts create mode 100644 tests/unit/shared/Stubs/EventSubscriptionCollectionStub.ts create mode 100644 tests/unit/shared/Stubs/EventSubscriptionStub.ts create mode 100644 tests/unit/shared/Stubs/ExecutableErrorContextStub.ts create mode 100644 tests/unit/shared/Stubs/ExecutablePermissionSetterStub.ts create mode 100644 tests/unit/shared/Stubs/ExecutableValidatorStub.ts create mode 100644 tests/unit/shared/Stubs/ExpressionEvaluationContextStub.ts create mode 100644 tests/unit/shared/Stubs/ExpressionParserStub.ts create mode 100644 tests/unit/shared/Stubs/ExpressionStub.ts create mode 100644 tests/unit/shared/Stubs/ExpressionsCompilerStub.ts create mode 100644 tests/unit/shared/Stubs/FileSystemOpsStub.ts create mode 100644 tests/unit/shared/Stubs/FilenameGeneratorStub.ts create mode 100644 tests/unit/shared/Stubs/FilterChangeDetailsStub.ts create mode 100644 tests/unit/shared/Stubs/FilterChangeDetailsVisitorStub.ts create mode 100644 tests/unit/shared/Stubs/FilterContextStub.ts create mode 100644 tests/unit/shared/Stubs/FilterResultStub.ts create mode 100644 tests/unit/shared/Stubs/FilterStrategyStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionCallArgumentStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionCallCompilationContextStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionCallCompilerStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionCallDataStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionCallStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionCallsParserStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionCodeStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionDataStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionParameterCollectionStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionParameterParserStub.ts create mode 100644 tests/unit/shared/Stubs/FunctionParameterStub.ts create mode 100644 tests/unit/shared/Stubs/HierarchyAccessStub.ts create mode 100644 tests/unit/shared/Stubs/IsArrayStub.ts create mode 100644 tests/unit/shared/Stubs/IsStringStub.ts create mode 100644 tests/unit/shared/Stubs/LanguageSyntaxStub.ts create mode 100644 tests/unit/shared/Stubs/LifecycleHookStub.ts create mode 100644 tests/unit/shared/Stubs/LocationOpsStub.ts create mode 100644 tests/unit/shared/Stubs/LoggerStub.ts create mode 100644 tests/unit/shared/Stubs/MarkdownRendererStub.ts create mode 100644 tests/unit/shared/Stubs/NodeMetadataStub.ts create mode 100644 tests/unit/shared/Stubs/NodeStateChangeEventArgsStub.ts create mode 100644 tests/unit/shared/Stubs/NodeStateChangedEventStub.ts create mode 100644 tests/unit/shared/Stubs/OperatingSystemOpsStub.ts create mode 100644 tests/unit/shared/Stubs/ParameterDefinitionDataStub.ts create mode 100644 tests/unit/shared/Stubs/ParameterNameValidatorStub.ts create mode 100644 tests/unit/shared/Stubs/PipeFactoryStub.ts create mode 100644 tests/unit/shared/Stubs/PipeStub.ts create mode 100644 tests/unit/shared/Stubs/PipelineCompilerStub.ts create mode 100644 tests/unit/shared/Stubs/PowerShellInvokeShellCommandCreatorStub.ts create mode 100644 tests/unit/shared/Stubs/ProjectDetailsParserStub.ts create mode 100644 tests/unit/shared/Stubs/ProjectDetailsStub.ts create mode 100644 tests/unit/shared/Stubs/QueryableNodesStub.ts create mode 100644 tests/unit/shared/Stubs/ReadbackFileWriterStub.ts create mode 100644 tests/unit/shared/Stubs/RenderQueueOrdererStub.ts create mode 100644 tests/unit/shared/Stubs/RepositoryEntityStub.ts create mode 100644 tests/unit/shared/Stubs/RuntimeEnvironmentStub.ts create mode 100644 tests/unit/shared/Stubs/SanityCheckOptionsStub.ts create mode 100644 tests/unit/shared/Stubs/SanityValidatorStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptCodeFactoryStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptCodeStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptCompilerStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptDataStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptDiagnosticsCollectorStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptDirectoryProviderStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptFactoryStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptFileCreatorStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptFileExecutorStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptParserStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptSelectionStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptingDefinitionDataStub.ts create mode 100644 tests/unit/shared/Stubs/ScriptingDefinitionStub.ts create mode 100644 tests/unit/shared/Stubs/SelectedScriptStub.ts create mode 100644 tests/unit/shared/Stubs/SharedFunctionCollectionStub.ts create mode 100644 tests/unit/shared/Stubs/SharedFunctionStub.ts create mode 100644 tests/unit/shared/Stubs/SharedFunctionsParserStub.ts create mode 100644 tests/unit/shared/Stubs/ShellArgumentEscaperStub.ts create mode 100644 tests/unit/shared/Stubs/ShellCommandRunnerStub.ts create mode 100644 tests/unit/shared/Stubs/SingleCallCompilerStrategyStub.ts create mode 100644 tests/unit/shared/Stubs/SingleCallCompilerStub.ts create mode 100644 tests/unit/shared/Stubs/SingleNodeFocusManagerStub.ts create mode 100644 tests/unit/shared/Stubs/SizeObserverStub.ts create mode 100644 tests/unit/shared/Stubs/StubWithObservableMethodCalls.ts create mode 100644 tests/unit/shared/Stubs/SyntaxFactoryStub.ts create mode 100644 tests/unit/shared/Stubs/SystemOperationsStub.ts create mode 100644 tests/unit/shared/Stubs/ThrottleStub.ts create mode 100644 tests/unit/shared/Stubs/TimeoutStub.ts create mode 100644 tests/unit/shared/Stubs/TimerStub.ts create mode 100644 tests/unit/shared/Stubs/TreeInputNodeDataStub.ts create mode 100644 tests/unit/shared/Stubs/TreeNodeCollectionStub.ts create mode 100644 tests/unit/shared/Stubs/TreeNodeParserStub.ts create mode 100644 tests/unit/shared/Stubs/TreeNodeStateAccessStub.ts create mode 100644 tests/unit/shared/Stubs/TreeNodeStateChangedEmittedEventStub.ts create mode 100644 tests/unit/shared/Stubs/TreeNodeStateDescriptorStub.ts create mode 100644 tests/unit/shared/Stubs/TreeNodeStateTransactionStub.ts create mode 100644 tests/unit/shared/Stubs/TreeNodeStub.ts create mode 100644 tests/unit/shared/Stubs/TreeRootStub.ts create mode 100644 tests/unit/shared/Stubs/TypeValidatorStub.ts create mode 100644 tests/unit/shared/Stubs/UseApplicationStub.ts create mode 100644 tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub.ts create mode 100644 tests/unit/shared/Stubs/UseClipboardStub.ts create mode 100644 tests/unit/shared/Stubs/UseCollectionStateStub.ts create mode 100644 tests/unit/shared/Stubs/UseCurrentCodeStub.ts create mode 100644 tests/unit/shared/Stubs/UseCurrentTreeNodesStub.ts create mode 100644 tests/unit/shared/Stubs/UseEventListenerStub.ts create mode 100644 tests/unit/shared/Stubs/UseNodeStateChangeAggregatorStub.ts create mode 100644 tests/unit/shared/Stubs/UseSvgLoaderStub.ts create mode 100644 tests/unit/shared/Stubs/UseUserSelectionStateStub.ts create mode 100644 tests/unit/shared/Stubs/UserSelectionStub.ts create mode 100644 tests/unit/shared/Stubs/VersionStub.ts create mode 100644 tests/unit/shared/Stubs/VueDependencyInjectionApiStub.ts create mode 100644 tests/unit/shared/Stubs/WindowVariablesStub.ts create mode 100644 tests/unit/shared/TestCases/AbsentTests.ts create mode 100644 tests/unit/shared/TestCases/SingletonFactoryTests.ts create mode 100644 tests/unit/shared/TestCases/TransientFactoryTests.ts create mode 100644 tsconfig.json create mode 100644 vite-config-helper.ts create mode 100644 vite.config.ts diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000..214388fe --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not dead diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..29d27048 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +dist_electron +.vs +.vscode +.github +.git +docs +docker \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..10bc0533 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,33 @@ +root = true # Top-most EditorConfig file + +[*] +end_of_line = lf + +[*.{js,jsx,ts,tsx,vue,sh,scss}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 100 + +[{Dockerfile}] +indent_style = space +indent_size = 4 + +[*.py] +indent_size = 4 # PEP 8 (the official Python style guide) recommends using 4 spaces per indentation level +indent_style = space +max_line_length = 100 + +[*.ps1] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{scss}] # SASS guidelines: https://archive.today/2024.02.16-232553/https://sass-guidelin.es/ +indent_style = space +indent_size = 2 # Recommended by SASS guidelines +max_line_length = 100 # Recommended by SASS guidelines +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..1d9f528f --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,96 @@ +const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style'); +const tsconfigJson = require('./tsconfig.json'); +require('@rushstack/eslint-patch/modern-module-resolution'); + +module.exports = { + root: true, + env: { + node: true, + es2022: true, // add globals and sets parserOptions.ecmaVersion to 2022 + }, + extends: [ + // Vue specific base rules, `eslint-plugin-vue` + 'plugin:vue/vue3-recommended', + + // Extends `eslint-config-airbnb` + '@vue/eslint-config-airbnb-with-typescript', + + // - Sets base parser and plugin options. + // - Includes `plugin:@typescript-eslint/recommended`. But incompatible with + // `strict-type-checked` and `stylistic-type-checked`, see https://github.com/vuejs/eslint-config-typescript/issues/67. + '@vue/typescript/recommended', + ], + rules: { + ...getOwnRules(), + ...getTurnedOffBrokenRules(), + ...getOpinionatedRuleOverrides(), + ...getTodoRules(), + }, +}; + +function getOwnRules() { + return { + 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'linebreak-style': ['error', 'unix'], // This is also enforced in .editorconfig and .gitattributes files + 'import/order': [ // Enforce strict import order taking account into aliases + 'error', + { + groups: [ // Enforce more strict order than AirBnb + 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], + pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external" + ...getAliasesFromTsConfig(), + 'js-yaml-loader!@/**', + ].map((pattern) => ({ pattern, group: 'internal' })), + }, + ], + }; +} + +function getTodoRules() { // Should be worked on separate future commits + return { + 'import/no-extraneous-dependencies': 'off', + // Accessibility improvements: + 'vuejs-accessibility/form-control-has-label': 'off', + 'vuejs-accessibility/click-events-have-key-events': 'off', + 'vuejs-accessibility/anchor-has-content': 'off', + 'vuejs-accessibility/accessible-emoji': 'off', + }; +} + +function getTurnedOffBrokenRules() { + return { + // Broken in TypeScript + 'no-useless-constructor': 'off', // Cannot interpret TypeScript constructors + 'no-shadow': 'off', // Fails with TypeScript enums + }; +} + +function getOpinionatedRuleOverrides() { + return { + // https://erkinekici.com/articles/linting-trap#no-use-before-define + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': 'off', + // https://erkinekici.com/articles/linting-trap#arrow-body-style + 'arrow-body-style': 'off', + // https://erkinekici.com/articles/linting-trap#no-plusplus + 'no-plusplus': 'off', + // https://erkinekici.com/articles/linting-trap#no-param-reassign + 'no-param-reassign': 'off', + // https://erkinekici.com/articles/linting-trap#class-methods-use-this + 'class-methods-use-this': 'off', + // https://erkinekici.com/articles/linting-trap#importprefer-default-export + 'import/prefer-default-export': 'off', + // https://erkinekici.com/articles/linting-trap#disallowing-for-of + // Original: https://github.com/airbnb/javascript/blob/d8cb404da74c302506f91e5928f30cc75109e74d/packages/eslint-config-airbnb-base/rules/style.js#L333-L351 + 'no-restricted-syntax': [ + baseStyleRules['no-restricted-syntax'][0], + ...baseStyleRules['no-restricted-syntax'].slice(1).filter((rule) => rule.selector !== 'ForOfStatement'), + ], + }; +} + +function getAliasesFromTsConfig() { + return Object.keys(tsconfigJson.compilerOptions.paths) + .map((path) => `${path}*`); +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e6be08df --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Prevent Git from auto-converting to CRLF on Windows, and convert to LF on checkin. +# * : All files +# text=auto : If Git decides content it text, it converts CRLF to LF on checkin. +# eol=lf : forces Git to normalize line endings to LF on checkin and prevents conversion +# to CRLF when the file is checked out. +* text=auto eol=lf \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..3e94d78f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: undergroundwires \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml b/.github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml new file mode 100644 index 00000000..77fde0d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml @@ -0,0 +1,114 @@ +name: "Bug Report: Script Issues" +description: 🐛 Report issues with generated scripts to enhance privacy.sexy +labels: [ 'bug' ] +title: '[Bug]: ' +body: + - + type: markdown + attributes: + value: |- + Thank you for contributing to privacy.sexy and guiding our direction! 🌟 + Please complete as much of the form below as possible. + Your feedback is valuable, even if you can't provide all details. + - + type: textarea + attributes: + label: Description + description: A clear and concise description of what the bug is. + placeholder: >- + For example: "After running the cleanup script, music playback stopped functioning." + validations: + required: true + - + type: textarea + attributes: + label: How can the bug be recreated? + description: |- + This is the most important information in the bug report. + Bugs that cannot be reproduced cannot be fixed or verified. + placeholder: |- + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - + type: textarea + attributes: + label: Operating system + description: |- + Please specify your operating system and its version. + + - On Windows: Open "Start button" > "Settings" > "System" > "About". + - On macOS: Open "Apple menu (top left corner)" > "About This Mac". + - On Linux: Open terminal > type: lsb_release -a > copy paste the result. + placeholder: >- + For example: "Windows 11 Pro 22H3" + validations: + required: false + - + type: textarea + attributes: + label: Script file + description: |- + If applicable, share the generated privacy.sexy file. + + GitHub may restrict script file attachments. + Upload your script file to a service like [GitHub Gist](https://gist.github.com/) and share the link here. + + If you used the desktop version to run the script, it is already stored on your system. + See the [documentation to locate it](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#secure-script-executionstorage). + + > **💡 Tip:** You can attach script files by dragging them into this area. + placeholder: |- + Attach the script, or post GitHub Gist link. + For example: https://gist.github.com/privacysexy-forks/6d85ad8ca27acc8c6a5417d4af28c9b6. + validations: + required: false + - + type: textarea + attributes: + label: Screenshots + description: |- + If applicable, add screenshots to help explain your problem. + + > **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in. + placeholder: Attach screenshots here or link to image hosting. + validations: + required: false + - + type: textarea + attributes: + label: Additional information + description: |- + If applicable, add any other context about the problem here. + + Helpful information includes: + + - Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging). + - Terminal output + - Proposed solutions + - Other related context such as related issues, software behavior, etc. + + > **💡 Tip:** You can attach log files by dragging them into this area. + placeholder: >- + For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..." + validations: + required: false + - + type: markdown + attributes: + value: |- + --- + + **✉️ A friendly note from the maintainer:** + + > [!NOTE] + > We are a small open-source project with a small community. + > It can sometimes take a long time for issues to be addressed, so please be patient. + > Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️. + > But your issue will eventually get attention regardless. + >

@undergroundwires

+ + --- diff --git a/.github/ISSUE_TEMPLATE/2-bug-report-general.yaml b/.github/ISSUE_TEMPLATE/2-bug-report-general.yaml new file mode 100644 index 00000000..308c0740 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-bug-report-general.yaml @@ -0,0 +1,104 @@ +name: "Bug Report: General" +description: 🐛 Report general issues to enhance privacy.sexy +labels: [ 'bug' ] +title: '[Bug]: ' +body: + - + type: markdown + attributes: + value: |- + Thank you for contributing to privacy.sexy and guiding our direction! 🌟 + Please complete as much of the form below as possible. + Your feedback is valuable, even if you can't provide all details. + - + type: textarea + attributes: + label: Description + description: Provide a clear and concise description of the issue. + placeholder: >- + For example: "I cannot select any scripts." + validations: + required: true + - + type: textarea + attributes: + label: Reproduction steps + description: |- + This is the most important information in the bug report. + Bugs that cannot be reproduced cannot be fixed or verified. + placeholder: |- + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - + type: textarea + attributes: + label: Expected behavior + description: Describe what you expected to happen when the error occurred. + placeholder: >- + For example: "I expected the settings menu to open smoothly without crashing.". + validations: + required: true + - + type: textarea + attributes: + label: Screenshots + description: |- + If applicable, add screenshots to help explain your problem. + + > **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in. + placeholder: >- + Attach screenshots here or link to image hosting. + validations: + required: false + - + type: textarea + attributes: + label: privacy.sexy environment details + description: |- + If applicable, mention how you were using privacy.sexy when the bug occurred: + + - Web (on which operating system and browser?) + - Or desktop (Windows, macOS, or Linux?) + placeholder: >- + For example: "The web version on Edge browser on Windows 11 23H2." + validations: + required: false + - + type: textarea + attributes: + label: Additional context + description: |- + If applicable, add any other context about the problem here. + + Helpful information includes: + + - Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging). + - Terminal output + - Proposed solutions + - Other related context such as related issues, software behavior, etc. + + > **💡 Tip:** You can attach log files by dragging them into this area. + placeholder: >- + For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..." + validations: + required: false + - + type: markdown + attributes: + value: |- + --- + + **✉️ A friendly note from the maintainer:** + + > [!NOTE] + > We are a small open-source project with a small community. + > It can sometimes take a long time for issues to be addressed, so please be patient. + > Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️. + > But your issue will eventually get attention regardless. + >

@undergroundwires

+ + --- diff --git a/.github/ISSUE_TEMPLATE/3-suggestion-feature.yaml b/.github/ISSUE_TEMPLATE/3-suggestion-feature.yaml new file mode 100644 index 00000000..a26d83f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-suggestion-feature.yaml @@ -0,0 +1,73 @@ +name: "Suggestion: Feature" +description: 💡 Suggest new ideas to enhance privacy.sexy +labels: [ 'enhancement' ] +title: '[Feature]: ' +body: + - + type: markdown + attributes: + value: |- + Thank you for contributing to privacy.sexy and guiding our direction! 🌟 + Please complete as much of the form below as possible. + Your feedback is valuable, even if you can't provide all details. + - + type: textarea + attributes: + label: Problem statement + description: |- + What are we trying to solve? + + Please add a clear and concise description of the problem you are seeking to solve with this feature request. + placeholder: >- + For example: "Every time I use the app, I struggle with..." + validations: + required: true + - + type: textarea + attributes: + label: Proposed solution + description: |- + Describe the solution you'd like in a clear and concise manner. + placeholder: >- + For example: "It would be great if the app could..." + validations: + required: true + - + type: textarea + attributes: + label: Alternatives considered + description: |- + Have you considered any alternative solutions or features? + Different perspectives can inspire new ideas. + placeholder: >- + For example: "We could also solve it by...". + validations: + required: false + - + type: textarea + attributes: + label: Additional information + description: |- + If applicable, add any other context or screenshots about the feature request here. + + > **💡 Tip:** You can attach files or screenshots by dragging them into this area. + placeholder: >- + For example: "Challenges can be ..., but I'm unsure about ..., here is some documentation about it: ..." + validations: + required: false + - + type: markdown + attributes: + value: |- + --- + + **✉️ A friendly note from the maintainer:** + + > [!NOTE] + > We are a small open-source project with a small community. + > It can sometimes take a long time for issues to be addressed, so please be patient. + > Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️. + > But your issue will eventually get attention regardless. + >

@undergroundwires

+ + --- diff --git a/.github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml b/.github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml new file mode 100644 index 00000000..f8c9f2a7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml @@ -0,0 +1,133 @@ +name: "Suggestion: New Script" +description: 💡 Suggest new scripts to enhance privacy.sexy +labels: [ 'enhancement' ] +title: '[New script]: ' +body: + - + type: markdown + attributes: + value: |- + Thank you for contributing to privacy.sexy and guiding our direction! 🌟 + Please complete as much of the form below as possible. + Your feedback is valuable, even if you can't provide all details. + + For guidance, see our [script guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md). + Consider submitting a PR to get your script added more quickly: (see [CONTRIBUTING.md](https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts)) + - + type: dropdown + attributes: + label: Operating system + description: Which operating system will the new script configure? + options: + - macOS + - Windows + - Linux + - All of them + validations: + required: false + - + type: textarea + attributes: + label: Name of the script + description: |- + Suggest a name for the script that clearly describes its function. + + See [script naming conventions](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name) for best practices. + placeholder: E.g, "Disable error data submission" + validations: + required: true + - + type: textarea + attributes: + label: Documentation/References + description: |- + Provide any relevant documentation or references. + Prefer high-quality sources such as vendor documentation. + + See [documentation guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation) for best practices. + placeholder: >- + For example: "This script will disable the error data submission, see https://microsoft.com/...". + validations: + required: true + - + type: textarea + attributes: + label: Code + description: |- + If possible, provide or explain the code that the script should execute. + + See [script code guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code). + placeholder: |- + For example: "Set registry key like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "1"`". + validations: + required: false + - + type: textarea + attributes: + label: Revert code + description: |- + If applicable, provide revert code to restore the changes made by the script. + + The revert code restores changes to their default state before script execution. + + Leave blank for non-reversible scripts. + placeholder: |- + For example: "Revert to operating system default like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "0"`". + validations: + required: false + - + type: textarea + attributes: + label: Suggested category + description: |- + Suggest a category for the script. + + If unsure, leave blank for maintainers to decide. + placeholder: >- + For example: "Privacy Cleanup > Clear system logs" + - + type: dropdown + attributes: + label: Recommendation level + description: |- + Suggest a recommendation level for the script: + + - **Standard**: Recommended for most users without side-effects. + - **Strict**: Provides improved privacy at the cost of some functionality. + - **None**: For advanced users or specific needs. + + If unsure, leave blank for maintainers to decide. + options: + - Standard + - Strict + - None (do not recommend) + validations: + required: false + - + type: textarea + attributes: + label: Additional information + description: |- + If applicable, add any other context or screenshots about the script request here. + + > **💡 Tip:** You can attach additional documents or screenshots by dragging them into this area or pasting directly. + placeholder: >- + For example: "Challenges can be ..., but I am unsure about ..." + validations: + required: false + - + type: markdown + attributes: + value: |- + --- + + **✉️ A friendly note from the maintainer:** + + > [!NOTE] + > We are a small open-source project with a small community. + > It can sometimes take a long time for issues to be addressed, so please be patient. + > Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️. + > But your issue will eventually get attention regardless. + >

@undergroundwires

+ + --- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..e04d6956 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +# This file must be named `config.yml`. GitHub does not recognize the file if it is named `config.yaml`. +blank_issues_enabled: true +contact_links: + - name: Donate + url: https://undergroundwires.dev/donate/ + about: ❤️ Donate to support the free software you love to keep it alive. + # A separate link for reporting vulnerabilities is not included here because GitHub generates it automatically. diff --git a/.github/actions/force-ipv4/README.md b/.github/actions/force-ipv4/README.md new file mode 100644 index 00000000..bdf6c0ca --- /dev/null +++ b/.github/actions/force-ipv4/README.md @@ -0,0 +1,32 @@ +# force-ipv4 + +## Overview + +This GitHub action enforces IPv4 for all outgoing network requests. It addresses connectivity issues encountered in GitHub runners, where IPv6 requests may lead to timeouts due to the lack of IPv6 support [1] [2]. + +## Background + +Some applications attempt network connections over IPv6. +Such as requests made by Node's `fetch` API causes `UND_ERR_CONNECT_TIMEOUT` [3] [4] and similar issues [5]. +This happens when the software cannot handle this such as by using Happy Eyeballs [6] [7]. + +## Usage + +To use this action in your GitHub workflow, add the following step before any job that requires network access: + +```yaml +- name: Enforce IPv4 Connectivity + uses: ./.github/actions/force-ipv4 +``` + +## Note + +This action is a workaround addressing specific IPv6-related connectivity issues on GitHub runners and may not be necessary if GitHub's infrastructure evolves to fully support IPv6 in the future. + +[1]: https://archive.ph/2024.03.28-185829/https://github.com/actions/runner/issues/3138 "Actions Runner fails on IPv6 only host · Issue #3138 · actions/runner · GitHub | github.com" +[2]: https://archive.ph/2024.03.28-185838/https://github.com/actions/runner-images/issues/668 "IPv6 on GitHub-hosted runners · Issue #668 · actions/runner-images · GitHub | github.com" +[3]: https://archive.ph/2024.03.28-185847/https://github.com/actions/runner/issues/3213 "GitHub runner cannot send `fetch` with `node`, failing with IPv6 DNS error `UND_ERR_CONNECT_TIMEOUT` · Issue #3213 · actions/runner · GitHub | github.com" +[4]: https://archive.ph/2024.03.28-185853/https://github.com/actions/runner-images/issues/9540 "Cannot send outbound requests using node fetch, failing with IPv6 DNS error UND_ERR_CONNECT_TIMEOUT · Issue #9540 · actions/runner-images · GitHub | github.com" +[5]: https://archive.today/2024.03.30-113315/https://github.com/nodejs/node/issues/40537 "\"localhost\" favours IPv6 in node v17, used to favour IPv4 · Issue #40537 · nodejs/node · GitHub" +[6]: https://archive.ph/2024.03.28-185900/https://github.com/nodejs/node/issues/41625 "Happy Eyeballs support (address IPv6 issues in Node 17) · Issue #41625 · nodejs/node · GitHub | github.com" +[7]: https://archive.ph/2024.03.28-185910/https://github.com/nodejs/undici/issues/1531 "fetch times out in under 5 seconds · Issue #1531 · nodejs/undici · GitHub | github.com" diff --git a/.github/actions/force-ipv4/action.yml b/.github/actions/force-ipv4/action.yml new file mode 100644 index 00000000..23c9ef5c --- /dev/null +++ b/.github/actions/force-ipv4/action.yml @@ -0,0 +1,12 @@ +inputs: + project-root: + required: false + default: '.' +runs: + using: composite + steps: + - + name: Run prefer IPv4 script + shell: bash + run: ./.github/actions/force-ipv4/force-ipv4.sh + working-directory: ${{ inputs.project-root }} diff --git a/.github/actions/force-ipv4/force-ipv4.sh b/.github/actions/force-ipv4/force-ipv4.sh new file mode 100644 index 00000000..b5ca7a66 --- /dev/null +++ b/.github/actions/force-ipv4/force-ipv4.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +main() { + if is_linux; then + echo 'Configuring Linux...' + + configure_warp_with_doh_and_ipv6_exclusion_on_linux # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support. + prefer_ipv4_on_linux # [DOES NOT WORK] It does not resolve the issue when run independently on GitHub runners without IPv6 support. + + # Considered alternatives: + # - `sysctl` commands, and direct changes to `/proc/sys/net/` and `/etc/sysctl.conf` led to silent + # Node 18 exits (code: 13) when using `fetch`. + elif is_macos; then + echo 'Configuring macOS...' + + configure_warp_with_doh_and_ipv6_exclusion_on_macos # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support. + disable_ipv6_on_macos # [WORKS INCONSISTENTLY] Resolves the issue inconsistently when run independently on GitHub runners without IPv6 support. + fi + echo "IPv4: $(curl --ipv4 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)" + echo "IPv6: $(curl --ipv6 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)" +} + +is_linux() { + [[ "$(uname -s)" == "Linux" ]] +} + +is_macos() { + [[ "$(uname -s)" == "Darwin" ]] +} + +configure_warp_with_doh_and_ipv6_exclusion_on_linux() { + install_warp_on_debian + configure_warp_doh_and_exclude_ipv6 +} + +configure_warp_with_doh_and_ipv6_exclusion_on_macos() { + brew install cloudflare-warp + configure_warp_doh_and_exclude_ipv6 +} + +configure_warp_doh_and_exclude_ipv6() { + echo 'Beginning configuration of the Cloudflare WARP client with DNS-over-HTTPS and IPv6 exclusion...' + echo 'Initiating client registration with Cloudflare...' + warp-cli --accept-tos registration new + echo 'Configuring WARP to operate in DNS-over-HTTPS mode (warp+doh)...' + warp-cli --accept-tos mode warp+doh + echo 'Excluding IPv6 traffic from WARP by configuring it as a split tunnel...' + warp-cli --accept-tos add-excluded-route '::/0' # Exclude IPv6, forcing IPv4 resolution + # `tunnel ip add` does not work with IP ranges, see https://community.cloudflare.com/t/cant-cidr-for-split-tunnling/630834 + echo 'Establishing WARP connection...' + warp-cli --accept-tos connect +} + +install_warp_on_debian() { + curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg + echo "deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflare-client.list + sudo apt-get update + sudo apt-get install -y cloudflare-warp +} + +disable_ipv6_on_macos() { + networksetup -listallnetworkservices \ + | tail -n +2 \ + | while IFS= read -r interface; do + echo "Disabling IPv6 on: $interface..." + networksetup -setv6off "$interface" + done +} + +prefer_ipv4_on_linux() { + local -r gai_config_file_path='/etc/gai.conf' + if [ ! -f "$gai_config_file_path" ]; then + echo "Creating $gai_config_file_path since it doesn't exist..." + touch "$gai_config_file_path" + fi + echo "precedence ::ffff:0:0/96 100" | sudo tee -a "$gai_config_file_path" > /dev/null + echo "Configuration complete." +} + +main diff --git a/.github/actions/npm-install-dependencies/action.yml b/.github/actions/npm-install-dependencies/action.yml new file mode 100644 index 00000000..e0788a19 --- /dev/null +++ b/.github/actions/npm-install-dependencies/action.yml @@ -0,0 +1,12 @@ +inputs: + working-directory: + required: false + default: '.' +runs: + using: composite + steps: + - + name: Run `npm ci` with retries + shell: bash + run: npm run install-deps -- --ci + working-directory: ${{ inputs.working-directory }} diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml new file mode 100644 index 00000000..dc6d3c95 --- /dev/null +++ b/.github/actions/setup-node/action.yml @@ -0,0 +1,9 @@ +runs: + using: composite + steps: + - + name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20.x + # check-latest: true # Newest versions can potentially have undiscovered bugs or regressions diff --git a/.github/actions/upload-artifact/action.yaml b/.github/actions/upload-artifact/action.yaml new file mode 100644 index 00000000..f26c0b1e --- /dev/null +++ b/.github/actions/upload-artifact/action.yaml @@ -0,0 +1,15 @@ +inputs: + name: + required: true + path: + required: true + +runs: + using: composite + steps: + - + name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.name }} + path: ${{ inputs.path }} diff --git a/.github/workflows/checks.build.yaml b/.github/workflows/checks.build.yaml new file mode 100644 index 00000000..31cac1d9 --- /dev/null +++ b/.github/workflows/checks.build.yaml @@ -0,0 +1,109 @@ +name: checks.build + +on: + push: + pull_request: + +jobs: + build-web: + strategy: + matrix: + os: [ macos, ubuntu, windows ] + mode: [ + # Vite mode: https://vitejs.dev/guide/env-and-mode.html + development, # Used by `dev` command + production, # Used by `build` command + # Vitest mode: https://vitest.dev/guide/cli.html + test, # Used by Vitest + ] + fail-fast: false # Allows to see results from other combinations + runs-on: ${{ matrix.os }}-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Build web + run: npm run build -- --mode ${{ matrix.mode }} + - + name: Verify web build artifacts + run: npm run check:verify-build-artifacts -- --web + + build-desktop: + strategy: + matrix: + os: [ macos, ubuntu, windows ] + mode: [ + # electron-vite modes: https://electron-vite.org/guide/env-and-mode.html#global-env-variables + development, # Used by `dev` command + production, # Used by `build` and `preview` commands + ] + fail-fast: false # Allows to see results from other combinations + runs-on: ${{ matrix.os }}-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Prebuild desktop + run: npm run electron:prebuild -- --mode ${{ matrix.mode }} + - + name: Verify unbundled desktop build artifacts + run: npm run check:verify-build-artifacts -- --electron-unbundled + - + name: Build (bundle and package) desktop application + run: npm run electron:build -- --publish never + - + name: Verify bundled desktop build artifacts + run: npm run check:verify-build-artifacts -- --electron-bundled + + build-docker: + strategy: + matrix: + os: + - macos-13 # Downgraded due to lack of nested virtualization support in ARM-based runners (See: actions/runner-images#9460, actions/runner-images#9741, abiosoft/colima#1023) + - ubuntu-latest + # - windows-latest # Windows runners do not support Linux containers + fail-fast: false # Allows to see results from other combinations + runs-on: ${{ matrix.os }} + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Install Docker on macOS + if: contains(matrix.os, 'macos') # macOS runner is missing Docker + run: |- + # Install Docker + brew install docker + # Docker on macOS misses daemon due to licensing, so install colima as runtime + brew install colima + # Start the daemon + colima start + - + name: Build Docker image + run: docker build -t undergroundwires/privacy.sexy:latest . + - + name: Run Docker image on port 8080 + run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest + - + name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts. + uses: ./.github/actions/force-ipv4 + - + name: Check server is up and returns HTTP 200 + run: >- + node ./scripts/verify-web-server-status.js \ + --url http://localhost:8080 \ + --max-retries ${{ matrix.os == 'macos' && '90' || '30' }} diff --git a/.github/workflows/checks.desktop-runtime-errors.yaml b/.github/workflows/checks.desktop-runtime-errors.yaml new file mode 100644 index 00000000..6a01a277 --- /dev/null +++ b/.github/workflows/checks.desktop-runtime-errors.yaml @@ -0,0 +1,76 @@ +name: checks.desktop-runtime-errors +# Verifies desktop builds for Electron applications across multiple OS platforms (macOS ,Ubuntu, and Windows). + +on: + push: + pull_request: + +jobs: + run-check: + strategy: + matrix: + os: + - macos-latest # Apple silicon (ARM64) + - macos-13 # Intel-based (x86-64) + - ubuntu-latest + - windows-latest + fail-fast: false # Allows to see results from other combinations + runs-on: ${{ matrix.os }} + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Configure Ubuntu + if: contains(matrix.os, 'ubuntu') # macOS runner is missing Docker + shell: bash + run: |- + sudo apt update + + # Configure AppImage dependencies + sudo apt install -y libfuse2 + + # Configure DBUS (fixes `Failed to connect to the bus: Could not parse server address: Unknown address type`) + if ! command -v 'dbus-launch' &> /dev/null; then + echo 'DBUS does not exist, installing...' + sudo apt install -y dbus-x11 # Gives both dbus and dbus-launch utility + fi + sudo systemctl start dbus + DBUS_LAUNCH_OUTPUT=$(dbus-launch) + if [ $? -eq 0 ]; then + echo "${DBUS_LAUNCH_OUTPUT}" >> $GITHUB_ENV + else + echo 'Error: dbus-launch command did not execute successfully. Exiting.' >&2 + echo "${DBUS_LAUNCH_OUTPUT}" >&2 + exit 1 + fi + + # Configure fake (virtual) display + sudo apt install -y xvfb + sudo Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + echo "DISPLAY=:99" >> $GITHUB_ENV + + # Install ImageMagick for screenshots + sudo apt install -y imagemagick + + # Install xdotool and xprop (from x11-utils) for window title capturing + sudo apt install -y xdotool x11-utils + - + name: Test + shell: bash + run: |- + export SCREENSHOT=true + npm run check:desktop + - + name: Upload screenshot + if: always() # Run even if previous step fails + uses: ./.github/actions/upload-artifact + with: + name: screenshot-${{ matrix.os }} + path: screenshot.png diff --git a/.github/workflows/checks.external-urls.yaml b/.github/workflows/checks.external-urls.yaml new file mode 100644 index 00000000..30cff494 --- /dev/null +++ b/.github/workflows/checks.external-urls.yaml @@ -0,0 +1,30 @@ +name: checks.external-urls + +on: + push: + schedule: + - cron: '0 0 * * 0' # at 00:00 on every Sunday + +jobs: + run-check: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts. + uses: ./.github/actions/force-ipv4 + - + name: Test + run: npm run check:external-urls + env: + RANDOMIZED_URL_CHECK_LIMIT: "${{ github.event_name == 'push' && '100' || '3000' }}" + # - Scheduled checks has high limit for thorough testing. + # - For push events, triggered by code changes, the amount of URLs are limited to provide quick feedback. diff --git a/.github/workflows/checks.quality.yaml b/.github/workflows/checks.quality.yaml new file mode 100644 index 00000000..e8e34b8f --- /dev/null +++ b/.github/workflows/checks.quality.yaml @@ -0,0 +1,101 @@ +name: checks.quality + +on: [ push, pull_request ] + +jobs: + lint: + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + lint-command: + - npm run lint:eslint + - npm run lint:yaml + - npm run lint:md + - npm run lint:md:relative-urls + - npm run lint:md:consistency + os: [ macos, ubuntu, windows ] + fail-fast: false # Still interested to see results from other combinations + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Lint + run: ${{ matrix.lint-command }} + + todo-check: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Scan latest commit for TODO comments + shell: bash + run: |- + readonly todo_comment_search_pattern='TODO'':' # Define search pattern in parts to prevent IDE from flagging this script line as a TODO item + if git grep "$todo_comment_search_pattern" HEAD; then + echo 'TODO comments found in the latest commit.' + exit 1 + else + echo 'No TODO comments found in the latest commit.' + exit 0 + fi + + pylint: + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + os: [ macos, ubuntu, windows ] + fail-fast: false # Still interested to see results from other combinations + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - + name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - + name: Analyzing the code with pylint + run: npm run lint:pylint + + validate-collection-files: + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + os: [ macos, ubuntu, windows ] + fail-fast: false # Still interested to see results from other combinations + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - + name: Install dependencies + run: python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt + - + name: Validate + run: python3 ./scripts/validate-collections-yaml diff --git a/.github/workflows/checks.scripts.yaml b/.github/workflows/checks.scripts.yaml new file mode 100644 index 00000000..45cd425c --- /dev/null +++ b/.github/workflows/checks.scripts.yaml @@ -0,0 +1,87 @@ +name: checks.scripts + +on: + push: + pull_request: + +jobs: + icons-build: + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + os: [ macos, ubuntu, windows ] + fail-fast: false # Still interested to see results from other combinations + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Install ImageMagick on macOS + if: matrix.os == 'macos' + run: brew install imagemagick + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Create icons + run: npm run icons:build + + install-deps: + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + install-deps-before: [true, false] + install-command: + - npm run install-deps + - npm run install-deps -- --no-errors + - npm run install-deps -- --ci + - npm run install-deps -- --fresh --non-deterministic + - npm run install-deps -- --fresh + - npm run install-deps -- --non-deterministic + os: [ macos, ubuntu, windows ] + fail-fast: false # Still interested to see results from other combinations + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + if: matrix.install-deps-before == true + uses: ./.github/actions/npm-install-dependencies + - + name: Run install-deps + run: ${{ matrix.install-command }} + + configure-vscode: + runs-on: ${{ matrix.os.name }}-latest + strategy: + matrix: + os: + - name: macos + install-vscode-command: brew install --cask visual-studio-code + - name: ubuntu + install-vscode-command: sudo snap install code --classic + - name: windows + install-vscode-command: choco install vscode + fail-fast: false # Still interested to see results from other combinations + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - + name: Install VSCode + run: ${{ matrix.os.install-vscode-command }} + - + name: Configure VSCode + run: python3 ./scripts/configure_vscode.py diff --git a/.github/workflows/checks.security.dependencies.yaml b/.github/workflows/checks.security.dependencies.yaml new file mode 100644 index 00000000..81ac0bbb --- /dev/null +++ b/.github/workflows/checks.security.dependencies.yaml @@ -0,0 +1,22 @@ +name: checks.security.dependencies + +on: + push: + pull_request: + paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change + schedule: + - cron: '0 0 * * 0' # at 00:00 on every Sunday + +jobs: + npm-audit: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: NPM audit + run: npm audit --omit=dev diff --git a/.github/workflows/checks.security.sast.yaml b/.github/workflows/checks.security.sast.yaml new file mode 100644 index 00000000..c345519e --- /dev/null +++ b/.github/workflows/checks.security.sast.yaml @@ -0,0 +1,42 @@ +name: checks.security.sast + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * 0' # at 00:00 on every Sunday + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ + javascript # analyzes code written in JavaScript, TypeScript and both. + ] + + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + - + name: Autobuild + uses: github/codeql-action/autobuild@v2 + - + name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/release.desktop.yaml b/.github/workflows/release.desktop.yaml new file mode 100644 index 00000000..6d8a4fee --- /dev/null +++ b/.github/workflows/release.desktop.yaml @@ -0,0 +1,42 @@ +name: release-desktop + +on: + release: + types: [created] # will be triggered when a NON-draft release is created and published. + +jobs: + publish-desktop-app: + name: ${{ matrix.os }} + strategy: + matrix: + os: [macos, ubuntu, windows] + fail-fast: false # So publish runs for other OSes if one fails + runs-on: ${{ matrix.os }}-latest + steps: + - + uses: actions/checkout@v4 + with: + ref: master # otherwise it defaults to the version tag missing bump commit + fetch-depth: 0 # fetch all history + - + name: Checkout to bump commit + shell: bash + run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)" + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Run unit tests + run: npm run test:unit + - + name: Prebuild + run: npm run electron:prebuild + - + name: Build and publish + run: npm run electron:build -- --publish always + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074 diff --git a/.github/workflows/release.git.yaml b/.github/workflows/release.git.yaml new file mode 100644 index 00000000..83ab1fb3 --- /dev/null +++ b/.github/workflows/release.git.yaml @@ -0,0 +1,17 @@ +name: release-git + +on: + push: # Ensure a new release is created for each new tag + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + bump-version-and-release: + if: github.event.base_ref == 'refs/heads/master' + runs-on: ubuntu-latest + steps: + - uses: undergroundwires/bump-everywhere@master + with: + user: undergroundwires-bot + release-token: ${{ secrets.BUMP_GITHUB_PAT }} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559 + # GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo. diff --git a/.github/workflows/release.site.yaml b/.github/workflows/release.site.yaml new file mode 100644 index 00000000..c71e8f65 --- /dev/null +++ b/.github/workflows/release.site.yaml @@ -0,0 +1,126 @@ +name: release-site + +on: + release: + types: [created] # will be triggered when a NON-draft release is created and published. + +jobs: + aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd + runs-on: ubuntu-latest + steps: + - + name: "Infrastructure: Checkout" + uses: actions/checkout@v4 + with: + path: aws + repository: undergroundwires/aws-static-site-with-cd + - + name: "Infrastructure: Create AWS user profile & session name" + run: >- + bash "scripts/configure/create-user-profile.sh" \ + --profile user \ + --access-key-id ${{secrets.AWS_DEPLOYMENT_USER_ACCESS_KEY_ID}} \ + --secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \ + --region us-east-1 \ + && \ + echo "SESSION_NAME=${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)" >> $GITHUB_ENV + working-directory: aws + - + name: "Infrastructure: Deploy IAM stack" + run: >- + bash "scripts/deploy/deploy-stack.sh" \ + --template-file stacks/iam-stack.yaml \ + --stack-name privacysexy-iam-stack \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides "WebStackName=privacysexy-web-stack DnsStackName=privacysexy-dns-stack \ + CertificateStackName=privacysexy-cert-stack RootDomainName=privacy.sexy" \ + --region us-east-1 --role-arn ${{secrets.AWS_IAM_STACK_DEPLOYMENT_ROLE_ARN}} \ + --profile user --session ${{ env.SESSION_NAME }} + working-directory: aws + - + name: "Infrastructure: Deploy DNS stack" + run: >- + bash "scripts/deploy/deploy-stack.sh" \ + --template-file stacks/dns-stack.yaml \ + --stack-name privacysexy-dns-stack \ + --parameter-overrides "RootDomainName=privacy.sexy" \ + --region us-east-1 \ + --role-arn ${{secrets.AWS_DNS_STACK_DEPLOYMENT_ROLE_ARN}} \ + --profile user --session ${{ env.SESSION_NAME }} + working-directory: aws + - + name: "Infrastructure: Deploy certificate stack" + run: >- + bash "scripts/deploy/deploy-stack.sh" \ + --template-file stacks/certificate-stack.yaml \ + --stack-name privacysexy-cert-stack \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides "IamStackName=privacysexy-iam-stack RootDomainName=privacy.sexy DnsStackName=privacysexy-dns-stack" \ + --region us-east-1 \ + --role-arn ${{secrets.AWS_CERTIFICATE_STACK_DEPLOYMENT_ROLE_ARN}} \ + --profile user --session ${{ env.SESSION_NAME }} + working-directory: aws + - + name: "Infrastructure: Deploy web stack" + run: >- + bash "scripts/deploy/deploy-stack.sh" \ + --template-file stacks/web-stack.yaml \ + --stack-name privacysexy-web-stack \ + --parameter-overrides "CertificateStackName=privacysexy-cert-stack DnsStackName=privacysexy-dns-stack \ + RootDomainName=privacy.sexy UseDeepLinks=true" \ + --capabilities CAPABILITY_IAM \ + --region us-east-1 \ + --role-arn ${{secrets.AWS_WEB_STACK_DEPLOYMENT_ROLE_ARN}} \ + --profile user --session ${{ env.SESSION_NAME }} + working-directory: aws + - + name: "App: Checkout" + uses: actions/checkout@v4 + with: + path: app + ref: master # otherwise we don't get version bump commit + - + name: "App: Setup node" + uses: ./app/.github/actions/setup-node + - + name: "App: Install dependencies" + uses: ./app/.github/actions/npm-install-dependencies + with: + working-directory: app + - + name: "App: Run unit tests" + run: npm run test:unit + working-directory: app + - + name: "App: Build" + run: npm run build + working-directory: app + - + name: "App: Verify web build artifacts" + run: npm run check:verify-build-artifacts -- --web + working-directory: app + - + name: "App: Deploy to S3" + shell: bash + run: |- + declare web_output_dir + if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then + echo 'Error: Could not determine distribution directory.' + exit 1 + fi + bash "aws/scripts/deploy/deploy-to-s3.sh" \ + --folder "${web_output_dir}" \ + --web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \ + --storage-class ONEZONE_IA \ + --role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \ + --region us-east-1 \ + --profile user --session ${{ env.SESSION_NAME }} + - + name: "App: Invalidate CloudFront cache" + run: >- + bash "aws/scripts/deploy/invalidate-cloudfront-cache.sh" \ + --paths "/*" \ + --web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \ + --role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \ + --region us-east-1 \ + --profile user --session ${{ env.SESSION_NAME }} diff --git a/.github/workflows/tests.e2e.yaml b/.github/workflows/tests.e2e.yaml new file mode 100644 index 00000000..3c16dca3 --- /dev/null +++ b/.github/workflows/tests.e2e.yaml @@ -0,0 +1,64 @@ +name: e2e-tests + +on: + push: + pull_request: + +jobs: + run-tests: + strategy: + matrix: + os: [macos, ubuntu, windows] + fail-fast: false # So it still runs on other OSes if one of them fails + runs-on: ${{ matrix.os }}-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Run e2e tests + run: npm run test:cy:run + - + name: Output artifact directories + id: artifacts + if: always() # Run even if previous steps fail because test run video is always captured + shell: bash + run: |- + declare -r dirs_json_file='cypress-dirs.json' + if [ ! -f "${dirs_json_file}" ]; then + echo "${dirs_json_file} does not exist" + exit 1 + fi + + SCREENSHOTS_DIR=$(jq -r '.screenshots' "${dirs_json_file}") + VIDEOS_DIR=$(jq -r '.videos' "${dirs_json_file}") + + for dir in "${SCREENSHOTS_DIR}" "${VIDEOS_DIR}"; do + if [ "${dir}" = 'null' ] || [ -z "${dir}" ]; then + echo "One or more directories are null or not specified in cypress-dirs.json" + exit 1 + fi + done + + echo "SCREENSHOTS_DIR=${SCREENSHOTS_DIR}" >> "${GITHUB_OUTPUT}" + echo "VIDEOS_DIR=${VIDEOS_DIR}" >> "${GITHUB_OUTPUT}" + - + name: Upload screenshots + if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed + uses: ./.github/actions/upload-artifact + with: + name: e2e-screenshots-${{ matrix.os }} + path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }} + - + name: Upload videos + if: always() # Run even if previous steps fail because test run video is always captured + uses: ./.github/actions/upload-artifact + with: + name: e2e-videos-${{ matrix.os }} + path: ${{ steps.artifacts.outputs.VIDEOS_DIR }} diff --git a/.github/workflows/tests.integration.yaml b/.github/workflows/tests.integration.yaml new file mode 100644 index 00000000..7cd9c856 --- /dev/null +++ b/.github/workflows/tests.integration.yaml @@ -0,0 +1,28 @@ +name: integration-tests + +on: + push: + pull_request: + schedule: # To get notified about problems from third party dependencies + - cron: '0 0 * * 0' # at 00:00 on every Sunday + +jobs: + run-tests: + strategy: + matrix: + os: [macos, ubuntu, windows] + fail-fast: false # So it still runs on other OSes if one of them fails + runs-on: ${{ matrix.os }}-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Run integration tests + run: npm run test:integration diff --git a/.github/workflows/tests.unit.yaml b/.github/workflows/tests.unit.yaml new file mode 100644 index 00000000..53853aa8 --- /dev/null +++ b/.github/workflows/tests.unit.yaml @@ -0,0 +1,26 @@ +name: unit-tests + +on: + push: + pull_request: + +jobs: + run-tests: + strategy: + matrix: + os: [macos, ubuntu, windows] + fail-fast: false # So it still runs on other OSes if one of them fails + runs-on: ${{ matrix.os }}-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set-up node + uses: ./.github/actions/setup-node + - + name: Install dependencies + uses: ./.github/actions/npm-install-dependencies + - + name: Run unit tests + run: npm run test:unit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..af927109 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Application build artifacts +/dist-*/ + +# npm +node_modules + +# Visual Studio Code +.vscode/**/* +!.vscode/extensions.json + +# draw.io +*.bkp +*.dtmp + +# macOS +.DS_Store + +# Python +__pycache__ +.venv \ No newline at end of file diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 00000000..3cbcbe14 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "default": true, + "MD013": false +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..a5f07626 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,26 @@ +{ + "recommendations": [ + // Common + "editorconfig.editorconfig", // Applies .editorconfig to follow project style. + "wengerk.highlight-bad-chars", // Highlights bad chars. + "wayou.vscode-todo-highlight", // Highlights TODO. + "wix.vscode-import-cost", // Shows in KB how much a require include in code. + // Markdown + "davidanson.vscode-markdownlint", // Lints markdown. + // YAML + "redhat.vscode-yaml", // Lints YAML files, validates against schema. + // TypeScript / JavaScript + "dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript. + "pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports. + // Vue + "Vue.volar", // Official Vue extensions + "Vue.vscode-typescript-vue-plugin", // Official TypeScript Vue Plugin + // Scripting + "timonwong.shellcheck", // Lints bash files. + "ms-vscode.powershell", // Lints PowerShell files. + "ms-python.python", // Python IntelliSense, debugging, and basic linting. + "ms-python.pylint", // Lints Python files + // Distribution + "ms-azuretools.vscode-docker" // Adds Docker support. + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..eea2bdde --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,959 @@ +# Changelog + +## 0.13.6 (2024-08-13) + +* win: improve service disabling as TrustedInstaller | [5d365f6](https://github.com/undergroundwires/privacy.sexy/commit/5d365f65fa0e34925b16b2eac2af53c31e34e99a) +* Fix documentation button spacing on small screens | [70959cc](https://github.com/undergroundwires/privacy.sexy/commit/70959ccadafac5abcfa83e90cdb0537890b05f14) +* Fix close button overlap by scrollbar | [19ea8db](https://github.com/undergroundwires/privacy.sexy/commit/19ea8dbc5bc2dc436200cd40bf2a84c3fc3c6471) +* win: refactor version-specific actions | [0239b52](https://github.com/undergroundwires/privacy.sexy/commit/0239b523859d5c2b80033cc03f0248a9af35f28f) +* win: support Microsoft Store Firefox installations | [8d7a7eb](https://github.com/undergroundwires/privacy.sexy/commit/8d7a7eb434b2d83e32fa758db7e6798849bad41c) +* Refactor text utilities and expand their usage | [851917e](https://github.com/undergroundwires/privacy.sexy/commit/851917e049c41c679644ddbe8ad4b6e45e5c8f35) +* Bump dependencies to latest | [dd7239b](https://github.com/undergroundwires/privacy.sexy/commit/dd7239b8c14027274926279a4c8c7e5845b55558) +* Refactor styles to match new CSS nesting behavior | [abe03ce](https://github.com/undergroundwires/privacy.sexy/commit/abe03cef3f691f6e56faee991cd2da9c45244279) +* Improve compiler error display for latest Chromium | [b16e136](https://github.com/undergroundwires/privacy.sexy/commit/b16e13678ce1b8a6871eba8196e82bb321410067) +* Fix intermittent `ModalDialog` unit test failures | [a650558](https://github.com/undergroundwires/privacy.sexy/commit/a6505587bf4a448f5f3de930004a95ee203416b8) +* Ensure tests do not log warning or errors | [ae0165f](https://github.com/undergroundwires/privacy.sexy/commit/ae0165f1fe7dba9dd8ddaa1afa722a939772d3b6) +* win: improve disabling SmartScreen #385 | [11e566d](https://github.com/undergroundwires/privacy.sexy/commit/11e566d0e5177214a2600f3fd2097aea62373b24) +* win: unify registry setting as TrustedInstaller | [8526d25](https://github.com/undergroundwires/privacy.sexy/commit/8526d2510b34cbd7e79342f79d444419f601b186) +* win: improve, fix, restructure CEIP disabling | [c2d3cdd](https://github.com/undergroundwires/privacy.sexy/commit/c2d3cddc47d8d4b34bff63d959612919fa971012) +* win: centralize, improve Defender data collection | [b185255](https://github.com/undergroundwires/privacy.sexy/commit/b185255a0a72d5bfa96d6cf60f868ecc67149d68) +* win: fix and document VStudio license removal | [109fc01](https://github.com/undergroundwires/privacy.sexy/commit/109fc01c9a047002c4309e7f8a2ca4647c494a8a) +* win: improve registry/recent cleaning | [48d97af](https://github.com/undergroundwires/privacy.sexy/commit/48d97afdf6c2964cab7951208e1b0a02c3fd4c9b) +* Relax linting to allow null recommendation | [6fbc816](https://github.com/undergroundwires/privacy.sexy/commit/6fbc81675f7f063c4ee2502b8d9f169aacb39ae4) +* Refactor executable IDs to use strings #262 | [ded55a6](https://github.com/undergroundwires/privacy.sexy/commit/ded55a66d6044a03d4e18330e146b69d159509a3) +* win: fix, improve and unify Windows version logic | [f89c232](https://github.com/undergroundwires/privacy.sexy/commit/f89c2322b05d19b82914b20416ecefd7bc7e3702) +* Fix PowerShell code block inlining in compiler | [d77c3cb](https://github.com/undergroundwires/privacy.sexy/commit/d77c3cbbe212d9929e083181cc331b45d01e2883) +* win: improve registry value deletion #381 | [55c23e9](https://github.com/undergroundwires/privacy.sexy/commit/55c23e9d4cee3b7f74c26a4ac8516535048d67f2) +* win: improve folder hiding in "This PC" #16 | [e8add5e](https://github.com/undergroundwires/privacy.sexy/commit/e8add5ec08d2e8b7636cc9c8f0f9a33e4b004265) +* win: improve Microsoft Edge associations removal | [c2f4b68](https://github.com/undergroundwires/privacy.sexy/commit/c2f4b6878635e97f9c4be7bf2ee194a2deebb38a) +* win: unify registry data setting, fix #380 | [4cea6b2](https://github.com/undergroundwires/privacy.sexy/commit/4cea6b26ec2717c792c2471cc587f370274f90c4) +* win: improve disabling NCSI #189, #216, #279 | [c7e57b8](https://github.com/undergroundwires/privacy.sexy/commit/c7e57b8913f409a1c149ba598dc2f8786df0f9a9) +* win, mac: fix minor typos, formatting, dead URLs | [29e1069](https://github.com/undergroundwires/privacy.sexy/commit/29e1069bf2bc317e3c255b38c1ba0ab078b42d98) +* win: fix, constrain and document WNS #227 #314 | [50ba00b](https://github.com/undergroundwires/privacy.sexy/commit/50ba00b0af6232fc9187532635b04c4d9d9a68af) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.5...0.13.6) + +## 0.13.5 (2024-06-26) + +* ci/cd: centralize and bump artifact uploads | [22d6c79](https://github.com/undergroundwires/privacy.sexy/commit/22d6c7991eb2c138578a7d41950f301906dbf703) +* win: document and improve Firefox telemetry #259 | [8341411](https://github.com/undergroundwires/privacy.sexy/commit/8341411be434c6d145e942b1792020ccf02f58c8) +* Add image to `README.md` to thank supporters | [fa2a92b](https://github.com/undergroundwires/privacy.sexy/commit/fa2a92bf893933bf5cd04512a712b7aa1b921277) +* win: improve executable blocking, Chrome reporting | [f21ef92](https://github.com/undergroundwires/privacy.sexy/commit/f21ef9250a2f459dbd4f789d857c78298fc202e6) +* mac: discourage and document captive portal script | [b29cd7b](https://github.com/undergroundwires/privacy.sexy/commit/b29cd7b5f74accf92c9700c3171670f82c8cb3b3) +* win: fix revert scripts for removing shortcuts | [8becc7d](https://github.com/undergroundwires/privacy.sexy/commit/8becc7dbc46af4441900e9841a716a53735bc82e) +* Refactor to unify scripts/categories as Executable | [c138f74](https://github.com/undergroundwires/privacy.sexy/commit/c138f74460bafaba3da55a65f3942bb6f95b1d99) +* Add object property validation in parser #369 | [6ecfa9b](https://github.com/undergroundwires/privacy.sexy/commit/6ecfa9b954edc10401acaf5c735eec0fc9f991cd) +* win: fix missing app access recommendations #369 | [1c2d82d](https://github.com/undergroundwires/privacy.sexy/commit/1c2d82dc9bd412ea601ab2550ba0b4f7d144f8e8) +* win: fix text and handwriting script omission #369 | [1a10cf2](https://github.com/undergroundwires/privacy.sexy/commit/1a10cf2e5f87cd8eb421ef77f6ce764b5482515e) +* mac: document, improve, encourage clearing logs | [e9a5285](https://github.com/undergroundwires/privacy.sexy/commit/e9a52859f63609c3f56def0b3e4d1ac6e5661536) +* Add schema validation for collection files #369 | [dc03bff](https://github.com/undergroundwires/privacy.sexy/commit/dc03bff324d673101002bb16f14e0429e8170fbb) +* win: fix incomplete VSCEIP, location scripts | [48761f6](https://github.com/undergroundwires/privacy.sexy/commit/48761f62a242f0910307994271cbe6730fb30f7e) +* Add type validation for parameters and fix types | [fac26a6](https://github.com/undergroundwires/privacy.sexy/commit/fac26a6ca07479c84fe62c5ea2a572dad1898ef8) +* Bump Electron to latest | [ed93614](https://github.com/undergroundwires/privacy.sexy/commit/ed93614ca34b1ab166e645cc5bedd497b0caeaac) +* Trim compiler error output for better readability | [78c62cf](https://github.com/undergroundwires/privacy.sexy/commit/78c62cfc953dbba543d8bdc42828a4ef4b13a7c7) +* win: fix errors due to missing Edge uninstaller | [2f82873](https://github.com/undergroundwires/privacy.sexy/commit/2f828735a87f98ba87b4fc826823d1482d4f2db2) +* win: fix latest Edge removal on Windows 10 #309 | [e7031a3](https://github.com/undergroundwires/privacy.sexy/commit/e7031a3ae4e57b6522c6ca67fc30e8a8718506b2) +* win: categorize, rename, doc Chrome & Edge scripts | [f286f92](https://github.com/undergroundwires/privacy.sexy/commit/f286f92b1fec49e89eea8982dffbc3d6ef1defde) +* win: add disabling Edge/WebView2 auto-updates #309 | [ed7e69c](https://github.com/undergroundwires/privacy.sexy/commit/ed7e69c07efe83fdb7f4af13aa220ff991fbbe59) +* win, linux, mac: fix typos #373 | [c09c5ff](https://github.com/undergroundwires/privacy.sexy/commit/c09c5ffa47865f7c76910644558b6783ed44f1e4) +* win: add more Edge scripts including AI & ads | [1430d52](https://github.com/undergroundwires/privacy.sexy/commit/1430d5215ab094d8201710761d631dc2bd740918) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.4...0.13.5) + +## 0.13.4 (2024-05-27) + +* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f) +* ci/cd: fix recent Docker build failures on macOS | [a1922c5](https://github.com/undergroundwires/privacy.sexy/commit/a1922c50c12b3b7806e9e681ace842194a178bda) +* win: standardize registry edit + delete on revert | [cec0b4b](https://github.com/undergroundwires/privacy.sexy/commit/cec0b4b4f63c3563a0e7923ce6324a38d71a3955) +* Fix e2e test failing on Windows | [4a7efa2](https://github.com/undergroundwires/privacy.sexy/commit/4a7efa27c8df73ef9b7960afed29f216b066cba2) +* Add support for macOS universal binary #348, #362 | [d25c4e8](https://github.com/undergroundwires/privacy.sexy/commit/d25c4e8c812b8d012010ba38070a2931dcd28908) +* Migrate to GitHub issue forms | [9ab3ff7](https://github.com/undergroundwires/privacy.sexy/commit/9ab3ff75b0a69ac2ba27dd02e82db9b5bd76ea0f) +* ci/cd: fix quality checks not running on all OSes | [2390530](https://github.com/undergroundwires/privacy.sexy/commit/2390530d929fb92c266558c52376569a0ecb90c1) +* Bump Vue to latest and fix universal selector CSS | [aae5434](https://github.com/undergroundwires/privacy.sexy/commit/aae54344511ec51d17ad0420a92cb5a064e0e7bb) +* Centralize and optimize `ResizeObserver` usage | [2923621](https://github.com/undergroundwires/privacy.sexy/commit/292362135db0519ec1050bab80ed373aad115731) +* win: improve app access disabling and docs #138 | [ff3d5c4](https://github.com/undergroundwires/privacy.sexy/commit/ff3d5c48419f663379f5aba8936636c22f2c5de8) +* win: document and discourage RSA key script #363 | [f347fde](https://github.com/undergroundwires/privacy.sexy/commit/f347fde0c85f8b51b0060fdea0a2724b042aaeed) +* win: improve printing removal /w Print Queue #279 | [150e067](https://github.com/undergroundwires/privacy.sexy/commit/150e0670392bb62348c20ec644a4ed8a6bbffe74) +* win: discourage blocking app access #121 #339 #350 | [7794846](https://github.com/undergroundwires/privacy.sexy/commit/77948461856e6837ddfbcbbef72a1bf9fc706b4e) +* Improve context for errors thrown by compiler | [4212c7b](https://github.com/undergroundwires/privacy.sexy/commit/4212c7b9e0b1500378a1e4e88efc2d59f39f3d29) +* win: document disabling firewall #115 #152 #364 | [12b1f18](https://github.com/undergroundwires/privacy.sexy/commit/12b1f183f7ce966d6ce090d98aeea7ec491f8c7c) +* win: add script to disable Recall feature | [ce4cfdd](https://github.com/undergroundwires/privacy.sexy/commit/ce4cfdd169b7da0edc3da61143c988ed5f3c976e) +* win, mac, linux: fix typos and dead URLs #367 | [9e34e64](https://github.com/undergroundwires/privacy.sexy/commit/9e34e644493674ca709b64a47206763d5d4bd60c) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.3...0.13.4) + +## 0.13.3 (2024-05-11) + +* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd) +* win: improve disabling SMBv1 protocol | [f584fab](https://github.com/undergroundwires/privacy.sexy/commit/f584fabb50c7de70ba43751d721af94d8fa2fa8a) +* win: improve disabling insecure renegotiations | [f261ab4](https://github.com/undergroundwires/privacy.sexy/commit/f261ab4cd9a53e31325e5c6da9129542971fe84b) +* win: doc, improve, encourage cipher disabling | [8b224ee](https://github.com/undergroundwires/privacy.sexy/commit/8b224eefe71be6a556a1085d8fe20dbd4b889430) +* ci/cd: add check for TODO comments | [4e21f05](https://github.com/undergroundwires/privacy.sexy/commit/4e21f05031d6cc90cda684bd598bec4735f8103b) +* win: improve 'Snipping Tool' removal #343 | [e18907c](https://github.com/undergroundwires/privacy.sexy/commit/e18907ca91e483255b44d14d7d923d7eef92afbd) +* ci/cd: lint Python scripts using `pylint` | [23bac0f](https://github.com/undergroundwires/privacy.sexy/commit/23bac0fc76ad697abb34f3fb327df5cdeb40286a) +* win: improve disabling insecure hashes #131 | [d19dde6](https://github.com/undergroundwires/privacy.sexy/commit/d19dde603ddac47022ee2e0ea865d53857560c26) +* Add system requirements documentation #134 | [0fc2ffc](https://github.com/undergroundwires/privacy.sexy/commit/0fc2ffc1ea36a9248c6a92da85a29f7b04b33796) +* win, linux, mac: fix various typos #349 | [694bf1a](https://github.com/undergroundwires/privacy.sexy/commit/694bf1a74d935531d7cd46891823af1fa58c3c8c) +* Fix script cancellation with new dialog on Linux | [8c17396](https://github.com/undergroundwires/privacy.sexy/commit/8c173962857a39dc0c9e5886cb2af4937e6618e7) +* win: improve disabling protocols | [4ef16ce](https://github.com/undergroundwires/privacy.sexy/commit/4ef16cea56789120cd041412d86b5577cccf0725) +* win: fix Copilot by excluding `r.bing.com` #329 | [66a5688](https://github.com/undergroundwires/privacy.sexy/commit/66a56888a4b3ead1a6bfef0feffa0218535701fe) +* Fix blank window on load on desktop version #348 | [813d820](https://github.com/undergroundwires/privacy.sexy/commit/813d820b85e1b623c50f8e0325ad372bf2f344f9) +* Improve desktop icon quality and generation | [ab25e0a](https://github.com/undergroundwires/privacy.sexy/commit/ab25e0a066be14ea979dafd0f80e1091bd5d33f8) +* win: improve enabling secure connections #175 | [c75df1c](https://github.com/undergroundwires/privacy.sexy/commit/c75df1c8c1151b64cbf014383dea0b748a8c78b3) +* Fix VSCode script issues with added CI/CD tests | [1d7cafc](https://github.com/undergroundwires/privacy.sexy/commit/1d7cafc831dcc339a10646794410dad7096bfe60) +* Fix win execution with whitespace in username #351 | [a334320](https://github.com/undergroundwires/privacy.sexy/commit/a3343205b1196d5a81fd3cee2ae661ce871a7bef) +* Fix misaligned tooltip positions in modal dialogs | [dd71536](https://github.com/undergroundwires/privacy.sexy/commit/dd71536316ec819caeb418b8635d544ac80e58ad) +* Fix Chromium scrollbar-induced layout shifts | [bc4879c](https://github.com/undergroundwires/privacy.sexy/commit/bc4879cfe97becac3c54f6b40780a89464d3b772) +* ci/cd: remove `check-latest` from `setup-node` | [52a4730](https://github.com/undergroundwires/privacy.sexy/commit/52a4730073b8ebfb2ce9d530b44e4a179f5849fe) +* win: categorize and rename network security #131 | [9fd193e](https://github.com/undergroundwires/privacy.sexy/commit/9fd193e676f1f0646898f5130fbfaaf25050b2e3) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.2...0.13.3) + +## 0.13.2 (2024-04-15) + +* Update documentation for `logo-update.js` script | [4a9b430](https://github.com/undergroundwires/privacy.sexy/commit/4a9b430702bc6082426b50ecc3a06362b5720796) +* win: improve and document removing Phone apps #279 | [8924337](https://github.com/undergroundwires/privacy.sexy/commit/89243371faa5d6aef5fce52b0d54a442143cdd39) +* Fix bottom gap in card expansion panel | [79183d6](https://github.com/undergroundwires/privacy.sexy/commit/79183d64173e588d88bf074d5b50a52a71c2d885) +* ci/cd: Fix macOS Docker build reliability issues | [8a5592f](https://github.com/undergroundwires/privacy.sexy/commit/8a5592f92be4366a806afc9eee9135696a1dd993) +* ci/cd: fix IPv6 timeouts with `force-ipv4` action | [52fadcd](https://github.com/undergroundwires/privacy.sexy/commit/52fadcd6177ed06216be9c67dad57192ae02a4f9) +* ci/cd: bump Node.js environment to 20.x | [59decd1](https://github.com/undergroundwires/privacy.sexy/commit/59decd17e273bada1493eaa855c43cbabf90308f) +* ci/cd: trigger URL checks more, and limit amount | [4fb6302](https://github.com/undergroundwires/privacy.sexy/commit/4fb6302c67f2a3fedff419e8c22872593cf800ef) +* Fix overflow in tree node content on small screens | [557cea3](https://github.com/undergroundwires/privacy.sexy/commit/557cea3f4866dc33236874f5fe4d2d69ee963dae) +* Fix horizontal layout shift after script selection | [bc7e1fa](https://github.com/undergroundwires/privacy.sexy/commit/bc7e1faa1c3f2b61bf2046fdd6d6a4141b484662) +* Fix card header expansion glitch on card collapse | [5d940b5](https://github.com/undergroundwires/privacy.sexy/commit/5d940b57ef2a4c219932cd15201401f8550cfb41) +* Ignore `ResizeObserver` errors in Cypress tests | [4472c28](https://github.com/undergroundwires/privacy.sexy/commit/4472c2852e4b87083bda7979471ab9f377d17a01) +* win: improve and document secret key scripts | [49f22f0](https://github.com/undergroundwires/privacy.sexy/commit/49f22f048f39e7388633c488b5fe59101b831984) +* Fix card arrow not being animated in sync | [7b546c5](https://github.com/undergroundwires/privacy.sexy/commit/7b546c567c4683a37fe94595362f4c2bf92ffd59) +* win: improve Windows feature disablement scripts | [b68711e](https://github.com/undergroundwires/privacy.sexy/commit/b68711ef88982c0ee2b1d41b4452e899821adc64) +* Fix top script menu overflow on small screens | [b7a20d9](https://github.com/undergroundwires/privacy.sexy/commit/b7a20d9d41ea8bcefdd553b87641f3c22b4cde97) +* win: fix Visual Studio remote analysis script #327 | [4142d08](https://github.com/undergroundwires/privacy.sexy/commit/4142d084f64a3b540487ff68b28032977d12006d) +* win: improve firewall docs /w `winget` impact #142 | [ffd647d](https://github.com/undergroundwires/privacy.sexy/commit/ffd647d1529375474b81900cc7bee4c32fbf861f) +* Centralize and use global spacing variables | [ae17200](https://github.com/undergroundwires/privacy.sexy/commit/ae172000a64416e5a3e2b2e32b7846f039f445f0) +* win: improve service revert and docs | [b87b7aa](https://github.com/undergroundwires/privacy.sexy/commit/b87b7aac7d118a23a0d1bfb881e385347de4adb7) +* Bump dependencies to latest, hold ESLint | [f3571ab](https://github.com/undergroundwires/privacy.sexy/commit/f3571abeafdbe1e6d152958fab26de91a9c08bc3) +* Fix inability to tap outside modal on mobile | [cb144ae](https://github.com/undergroundwires/privacy.sexy/commit/cb144ae47273deeb7058d4b1380e480ebccdaf81) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.1...0.13.2) + +## 0.13.1 (2024-03-22) + +* ci/cd: Fix cross-platform git command compability | [255c51c](https://github.com/undergroundwires/privacy.sexy/commit/255c51c8a0524d3ea8a3b16ffc1b178650525010) +* Fix tooltip falling behind elements on fade out | [1964524](https://github.com/undergroundwires/privacy.sexy/commit/19645248ab7bc78dc872fa176c1a3650d7d6d644) +* Improve VSCode detection in `configure_vscode.py` | [98845e6](https://github.com/undergroundwires/privacy.sexy/commit/98845e6caee168db131aaf0736533e450827a52c) +* Bump TypeScript to 5.3 with `verbatimModuleSyntax` | [a721e82](https://github.com/undergroundwires/privacy.sexy/commit/a721e82a4fb603c0732ccfdffc87396c2a01363e) +* Migrate to Vite 5 and adjust configurations | [4ac1425](https://github.com/undergroundwires/privacy.sexy/commit/4ac1425f76079352268c488f3ff607d1fdc1beb2) +* win: improve and unify service start/stop logic | [adc2089](https://github.com/undergroundwires/privacy.sexy/commit/adc20898873d50a8873ffc74c48257e69a45d367) +* Upgrade vitest to v1 and fix test definitions | [e721885](https://github.com/undergroundwires/privacy.sexy/commit/e7218850ba62a7bebaf4768b13e46cba0dedd906) +* Improve URL checks to reduce false-negatives | [5abf8ff](https://github.com/undergroundwires/privacy.sexy/commit/5abf8ff216a1da737fd489864eeee880f78d6601) +* win: improve OneDrive data deletion safety | [5eff3a0](https://github.com/undergroundwires/privacy.sexy/commit/5eff3a04886d0d23a6e4c13a0178bb247105c5cb) +* Bump Electron to latest and use native ESM | [840adf9](https://github.com/undergroundwires/privacy.sexy/commit/840adf9429ed47f9e88c05e90f1d3ab930c2dfc4) +* Fix tooltip styling inconsistency | [ec34ac1](https://github.com/undergroundwires/privacy.sexy/commit/ec34ac1124e8b8ae53bf31a4dbdc88bb078b3d4e) +* win: fix VSCode manual update switch script #312 | [b71ad79](https://github.com/undergroundwires/privacy.sexy/commit/b71ad797a3af0db45143249903cb5e178692de7c) +* mac, linux, win: fix dead URLs and improve docs | [abec9de](https://github.com/undergroundwires/privacy.sexy/commit/abec9def075d82fdaee9663ef8fe1a488911f45b) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.0...0.13.1) + +## 0.13.0 (2024-02-11) + +* win: add disabling clipboard features #251, #247 | [c6ebba8](https://github.com/undergroundwires/privacy.sexy/commit/c6ebba85fb1b362be0d81d3078f19db71e0528b2) +* win: improve search privacy scripts #117 | [541f9aa](https://github.com/undergroundwires/privacy.sexy/commit/541f9aa5ee1b5f4885063b65beaf6cd873f0d786) +* win: add disabling Windows Copilot #263, #266 | [cd42550](https://github.com/undergroundwires/privacy.sexy/commit/cd425502ae882bba9642dc2171c2b5771946b5a9) +* win: add Dropbox telemetry blocking #125, #118 | [10829d6](https://github.com/undergroundwires/privacy.sexy/commit/10829d65aa3fb0df937bb8829244e6290bb748c7) +* Improve selection type documentation | [7af8daa](https://github.com/undergroundwires/privacy.sexy/commit/7af8daa3411b24efb6385c7876a49bd372753f38) +* Expand script names to take full available width | [d277139](https://github.com/undergroundwires/privacy.sexy/commit/d277139dd50eeb4e4057b0a7d8fc4ac2d70785de) +* Limit tooltip width for improved readability | [6ab6dac](https://github.com/undergroundwires/privacy.sexy/commit/6ab6dacd1be2d7bf1863b07b121d86f2a379ac67) +* Add markdown support for script/category names | [a5ffed4](https://github.com/undergroundwires/privacy.sexy/commit/a5ffed4cd60d9d058d5374145c1176b10fad1660) +* Normalize and improve font sizes | [4da306b](https://github.com/undergroundwires/privacy.sexy/commit/4da306b9f79b0bb7a64bb197fb246258cf435b8d) +* Change 'revert' button to title case | [937f459](https://github.com/undergroundwires/privacy.sexy/commit/937f4593d1a91081ab6b1bcb8f85d03879d7cf07) +* Remove playful emojis (🍑🍆) | [aa4205f](https://github.com/undergroundwires/privacy.sexy/commit/aa4205ff7af7d05cfb5e82bf541b521d49bbd1c8) +* Improve UI code styling for all platforms | [311fcb1](https://github.com/undergroundwires/privacy.sexy/commit/311fcb18133d1343f6a9ae5bd7a25795a1d12c49) +* Render bracket references as superscript text | [b9c89b7](https://github.com/undergroundwires/privacy.sexy/commit/b9c89b701fc77d20dcc706419a8659ad156c4fc2) +* Change slogan and refactor project info naming | [a54e164](https://github.com/undergroundwires/privacy.sexy/commit/a54e16488ce32219bcf811b5da85f06584b293fb) +* Add 'Revert All Selection' feature #68 | [55fa7ea](https://github.com/undergroundwires/privacy.sexy/commit/55fa7eae71031357d6f03f0d349a09cd446270d3) +* win, mac, linux: add privacy.sexy cleanup scripts | [63366a4](https://github.com/undergroundwires/privacy.sexy/commit/63366a4ec2533a376849d692211e9972b56ab4a8) +* Extend search by including documentation content | [6142f3a](https://github.com/undergroundwires/privacy.sexy/commit/6142f3a2973d20493f784f323f3be57fa8deaeef) +* Remove 'preview' label from Linux options | [ebd8285](https://github.com/undergroundwires/privacy.sexy/commit/ebd82853ddc56f1cc2fc9be3fe0b3001b07f0186) +* Change fonts for improved readability | [d5bbc32](https://github.com/undergroundwires/privacy.sexy/commit/d5bbc321f902dc60618ffdfda0d583a4a433f7af) +* Apply global styles for visual consistency | [faa7a38](https://github.com/undergroundwires/privacy.sexy/commit/faa7a38a7d16390f27e4a3e51017b81665cf85ca) +* Add UI animations for expand/collapse actions | [fb08f03](https://github.com/undergroundwires/privacy.sexy/commit/fb08f037651e1a7d453b9a6af724cbccecc5b903) +* win: relocate service disabling and improve docs | [894687c](https://github.com/undergroundwires/privacy.sexy/commit/894687c0e0375a24f40bcd720ea69c9b2aa62a58) +* win: add host blocking category #26 | [17152c8](https://github.com/undergroundwires/privacy.sexy/commit/17152c84dc639e75560998a6feddfd46e0f713ce) +* Update meta title and description | [c7fa4b6](https://github.com/undergroundwires/privacy.sexy/commit/c7fa4b6d020ac6fd3bf72bb4e57022dffb1ba921) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.10...0.13.0) + +## 0.12.10 (2024-01-17) + +* Fix CSP for Vue, Ace, Vite, Safari compatibility | [940febc](https://github.com/undergroundwires/privacy.sexy/commit/940febc3e80cfd0c01b5cc8282ebaab6b024d1b5) +* Improve security by isolating code execution more | [efa05f4](https://github.com/undergroundwires/privacy.sexy/commit/efa05f42bc53c44a352152b7c272bc0bda363070) +* Fix unresponsive circle icon in revert button | [645c333](https://github.com/undergroundwires/privacy.sexy/commit/645c33378769969c525a1552c65f8d0005e25fcf) +* Improve documentation for contribution guidelines | [fc9dd23](https://github.com/undergroundwires/privacy.sexy/commit/fc9dd234e9c749247f42289432ebb92dbe0a5f64) +* Bump Node.js environment to 18.x | [2f06043](https://github.com/undergroundwires/privacy.sexy/commit/2f0604355988a421690bb275375c3df280af7ee6) +* Fix button inconsistencies and macOS layout shifts | [86fde6d](https://github.com/undergroundwires/privacy.sexy/commit/86fde6d7dc61bbeeb3088cd24e37451181cc4e01) +* win: fix language dependent delete script #149 | [8f4b34f](https://github.com/undergroundwires/privacy.sexy/commit/8f4b34f8f156476f56fb7dde8e7c762f4455518b) +* Improve desktop script runs with timestamps & logs | [cdc32d1](https://github.com/undergroundwires/privacy.sexy/commit/cdc32d1f12c938966238c9569c91b64b23cd6f26) +* win: improve store app docs and add research #279 | [fac72ed](https://github.com/undergroundwires/privacy.sexy/commit/fac72edd551264320ed97194e7ecb3fcc34139f7) +* Fix handling special chars in script paths | [40f5eb8](https://github.com/undergroundwires/privacy.sexy/commit/40f5eb8334b27e958eee63e2141ded7d5861d960) +* Fix macOS detection in desktop app and Chromium | [dc30825](https://github.com/undergroundwires/privacy.sexy/commit/dc30825232a1355a325e364c8cd9fde78ffa3b1a) +* Bump ESLint Typescript dependencies to latest | [bf7fb07](https://github.com/undergroundwires/privacy.sexy/commit/bf7fb0732c52745521c1a89b963bdbf3394d9e63) +* Fix script deletion during execution on desktop | [c84a1bb](https://github.com/undergroundwires/privacy.sexy/commit/c84a1bb74ccb7a53bd493684b63a9e04f40e0b8b) +* Fix script execution for Linux VSCode development | [3b1a89c](https://github.com/undergroundwires/privacy.sexy/commit/3b1a89ce863c18c32be7d0b22dba566f692d81d1) +* Fix touch, cursor and accessibility in slider | [7285842](https://github.com/undergroundwires/privacy.sexy/commit/728584240cae6b3857abca4d3ddaaa7f6bb4a66e) +* Fix invisible script execution on Windows #264 | [b404a91](https://github.com/undergroundwires/privacy.sexy/commit/b404a91ada509e19a287d026d55db0035ff6233b) +* win: add missing extension apps, improve docs #279 | [da4be50](https://github.com/undergroundwires/privacy.sexy/commit/da4be500da7b0b5897a8b3e0525d9e50c9159fe0) +* Show native save dialogs in desktop app #50, #264 | [c546a33](https://github.com/undergroundwires/privacy.sexy/commit/c546a33eff7506550c7bcf03bb1f227a7c091816) +* Show save/execution error dialogs on desktop #264 | [e09db0f](https://github.com/undergroundwires/privacy.sexy/commit/e09db0f1bd73503204d8e5375a9cfe693f174a57) +* Add Windows save instructions UI and fix URL #296 | [756c736](https://github.com/undergroundwires/privacy.sexy/commit/756c736e21d713b8d2651cf2a9d7cf0678badde0) +* Add AD detection on desktop app #264, #304 | [f03fc24](https://github.com/undergroundwires/privacy.sexy/commit/f03fc2409832ddf904bc6bd4e19274a8d40745dc) +* Improve script error dialogs #304 | [6ada8d4](https://github.com/undergroundwires/privacy.sexy/commit/6ada8d425c4a7df88490756187c84b5c57ed1dcc) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.9...0.12.10) + +## 0.12.9 (2023-12-16) + +* win: improve docs and category of jump lists #146 | [40ae8a8](https://github.com/undergroundwires/privacy.sexy/commit/40ae8a8addaeb834ee26eabd330fda5cbb495324) +* mac: improve clearing privacy permissions | [5a7d7d8](https://github.com/undergroundwires/privacy.sexy/commit/5a7d7d88ff2f3e8862b18c94d062f692ee4b690b) +* win: fix logic for terminating processes | [807ae6a](https://github.com/undergroundwires/privacy.sexy/commit/807ae6a8f8ca724d781169f3ecb40f43ccd3fe10) +* win: improve documentation for "Get Help" app #280 | [8f5d7ed](https://github.com/undergroundwires/privacy.sexy/commit/8f5d7ed3cfa57f66dded9b72374006c9b6df2ce9) +* Centralize log file and refactor desktop logging | [08dbfea](https://github.com/undergroundwires/privacy.sexy/commit/08dbfead7ca7b55fe85f7dded01f2d4b88906c72) +* win: fix revert and improve docs for SAM enum #255 | [25e23c8](https://github.com/undergroundwires/privacy.sexy/commit/25e23c89c3f86897d5661a24a774997c924d3b2d) +* Improve security and reliability of macOS updates | [4765752](https://github.com/undergroundwires/privacy.sexy/commit/4765752ee3a36301b3d97317c570432424de8460) +* win: fix Win 11 Windows Security app removal #195 | [daa6230](https://github.com/undergroundwires/privacy.sexy/commit/daa6230fc96f2cf7210bc8c165106c0d5544e5fb) +* Improve security and privacy with strict meta tags | [ba5b29a](https://github.com/undergroundwires/privacy.sexy/commit/ba5b29a35dd7665aeea430aec4aaa8ff5ca811de) +* win: document and discourage admin shares #249 | [e747ee5](https://github.com/undergroundwires/privacy.sexy/commit/e747ee5cbc7cf5f0fe28a87fe7d02457d777373e) +* win: discourage XboxIdentityProvider #64, #79 #181 | [c72f9f5](https://github.com/undergroundwires/privacy.sexy/commit/c72f9f501680c1d880a0b560d02451a9e31063b4) +* win: improve disabling update healing #272 | [47b4823](https://github.com/undergroundwires/privacy.sexy/commit/47b4823bc5e487188b12cbea67db2525260af497) +* Fix tooltip overflow on smaller screens | [916c9d6](https://github.com/undergroundwires/privacy.sexy/commit/916c9d62d9fce27c3cd3feaf90c66df584d4f04a) +* Fix touch state not being activated in iOS Safari | [a985127](https://github.com/undergroundwires/privacy.sexy/commit/a9851272ae14eb1b374767b0eed3eb68e6dd1560) +* Fix tree view alignment and padding issues | [15134ea](https://github.com/undergroundwires/privacy.sexy/commit/15134ea04bc46e8cb13977d75b788f5ff71c800e) +* win: improve disabling of Application Experience | [fe3de49](https://github.com/undergroundwires/privacy.sexy/commit/fe3de498c8a1394efd6517d436797a08f938bb57) +* Fix OS switching not working on tree view UI | [3457fe1](https://github.com/undergroundwires/privacy.sexy/commit/3457fe18cf8193883f45b50ecbc9638c91ace2fb) +* Fix touch-enabled Chromium highlight on tree nodes | [2063397](https://github.com/undergroundwires/privacy.sexy/commit/20633972e9b56bdc102357129e74df30a95cefa9) +* win: add scripts to postpone auto-updates #272 | [e95b2ba](https://github.com/undergroundwires/privacy.sexy/commit/e95b2ba2179e40c0033a51b0087871dbfdc32d78) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.8...0.12.9) + +## 0.12.8 (2023-11-27) + +* Remove duplicated `index.html` file | [aab0f7e](https://github.com/undergroundwires/privacy.sexy/commit/aab0f7ea4680f377c610066bd0e99011eed8b506) +* Refactor DI for simplicity and type safety | [7770a9b](https://github.com/undergroundwires/privacy.sexy/commit/7770a9b5211d7208cfb2bfa5f737d46dc90b7946) +* Refactor user selection state handling using hook | [58cd551](https://github.com/undergroundwires/privacy.sexy/commit/58cd551a304a03e42637e6858982f8c5dfd9f598) +* Refactor watch sources for reliability | [7ab16ec](https://github.com/undergroundwires/privacy.sexy/commit/7ab16ecccb31b2d54e5b634520a8246fbbc248c1) +* Refactor to enforce strictNullChecks | [949fac1](https://github.com/undergroundwires/privacy.sexy/commit/949fac1a7cbc962ed63058e6a896695cfb4d35c8) +* Fix icon tooltip alignment on instructions modal | [bd383ed](https://github.com/undergroundwires/privacy.sexy/commit/bd383ed273ca95c10ea1cce765c0aa6836ec508c) +* Fix mobile layout overflow caused by tooltips | [e541a35](https://github.com/undergroundwires/privacy.sexy/commit/e541a35e86c0eff83f84dd002b46de7c55ebbcac) +* win: improve disabling of scheduled tasks | [3864f04](https://github.com/undergroundwires/privacy.sexy/commit/3864f042180f62afe469fdfe36010b018f84f4b3) +* Fix card list UI layout shifts (jumps) on load | [bf3426f](https://github.com/undergroundwires/privacy.sexy/commit/bf3426f91b6b7dbcad58d58507222559a8d14242) +* Refactor to Vue 3 recommended ESLint rules | [4531645](https://github.com/undergroundwires/privacy.sexy/commit/4531645b4c0c5143f15240652368bb9b9ddb48a4) +* Fix code highlighting and optimize category select | [cb42f11](https://github.com/undergroundwires/privacy.sexy/commit/cb42f11b9785e74719338a0a80a50d81dfccb4b6) +* Fix layout jumps/shifts and overflow on modals | [e299d40](https://github.com/undergroundwires/privacy.sexy/commit/e299d40fa1d71d921d4dac37e469fe299c9da3af) +* win: fix and improve Store app categorization #190 | [094dbb0](https://github.com/undergroundwires/privacy.sexy/commit/094dbb01b83bce9925fafab778b922f64390c2be) +* win: fix persistent update disabling /w tasks #272 | [dee3279](https://github.com/undergroundwires/privacy.sexy/commit/dee3279f85c99a9c62201a093b1afa41ec2412ec) +* win: discourage IntelliCode disabling #267, #286 | [7f7a84e](https://github.com/undergroundwires/privacy.sexy/commit/7f7a84e3ba259fade22d4838563d16129a1585e6) +* Fix spacing in documentation for readability | [1442f62](https://github.com/undergroundwires/privacy.sexy/commit/1442f626335e30e3a8d74e4e13e561c41f073ef8) +* win: fix system app removal affecting updates #287 | [7c632f7](https://github.com/undergroundwires/privacy.sexy/commit/7c632f738853b32fd90952bb4ca1ac924f962eb0) +* Fix rendering of inline code blocks for docs | [9845a7c](https://github.com/undergroundwires/privacy.sexy/commit/9845a7cd68a9920c96da739b58238bb1fdb1251d) +* linux: fix Firefox settings not reverting #282 | [bcad357](https://github.com/undergroundwires/privacy.sexy/commit/bcad357017d9f29ce77e706ca943107dd9caefb6) +* Fix incorrect URL rendering in documentation texts | [d328f08](https://github.com/undergroundwires/privacy.sexy/commit/d328f0895244d998e885ad8df335b6444b9ac66b) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.7...0.12.8) + +## 0.12.7 (2023-11-07) + +* Add winget download instructions | [b2ffc90](https://github.com/undergroundwires/privacy.sexy/commit/b2ffc90da70367b9e65c82556e8f440f865ceb98) +* Fix unresponsive copy button on instructions modal | [8ccaec7](https://github.com/undergroundwires/privacy.sexy/commit/8ccaec7af6ea3ecfd46bab5c13b90f71d55e32c1) +* Fix tree node check states not being updated | [af7219f](https://github.com/undergroundwires/privacy.sexy/commit/af7219f6e12ab4a65ce07190f691cf3234e87e35) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.6...0.12.7) + +## 0.12.6 (2023-11-03) + +* Bump dependencies to latest | [25d7f7b](https://github.com/undergroundwires/privacy.sexy/commit/25d7f7b2a479e51e092881cc2751e67a7d3f179f) +* win: improve system app uninstall cleanup #73 | [dbe3c5c](https://github.com/undergroundwires/privacy.sexy/commit/dbe3c5cfb91ba8a1657838b69117858843c8fbc8) +* win: improve system app uninstall /w fallback #260 | [98a26f9](https://github.com/undergroundwires/privacy.sexy/commit/98a26f9ae47af2668aa53f39d1768983036048ce) +* Improve performance of rendering during search | [79b46bf](https://github.com/undergroundwires/privacy.sexy/commit/79b46bf21004d96d31551439e5db5d698a3f71f3) +* Fix YAML error for site release in CI/CD | [237d994](https://github.com/undergroundwires/privacy.sexy/commit/237d9944f900f5172366868d75219224ff0542b0) +* win: fix Microsoft Advertising app removal #200 | [e40b9a3](https://github.com/undergroundwires/privacy.sexy/commit/e40b9a3cf53c341f2e84023a9f0e9680ac08f3fa) +* win: improve directory cleanup security | [060e789](https://github.com/undergroundwires/privacy.sexy/commit/060e7896624309aebd25e8b190c127282de177e8) +* Centralize Electron entry file path configuration | [d6da406](https://github.com/undergroundwires/privacy.sexy/commit/d6da406c61e5b9f5408851d1302d6d7398157a2e) +* win: prevent updates from reinstalling apps #260 | [8570b02](https://github.com/undergroundwires/privacy.sexy/commit/8570b02dde14ffad64863f614682c3fc1f87b6c2) +* win: improve script environment robustness #221 | [dfd4451](https://github.com/undergroundwires/privacy.sexy/commit/dfd44515613f38abe5a806bda36f44e7b715b50b) +* Fix compiler failing with nested `with` expression | [80821fc](https://github.com/undergroundwires/privacy.sexy/commit/80821fca0769e5fd2c6338918fbdcea12fbe83d2) +* win: improve soft file/app delete security #260 | [f4a74f0](https://github.com/undergroundwires/privacy.sexy/commit/f4a74f058db9b5bcbcbe438785db5ec88ecc1657) +* Fix incorrect tooltip position after window resize | [f8e5f1a](https://github.com/undergroundwires/privacy.sexy/commit/f8e5f1a5a2afa1f18567e6d965359b6a1f082367) +* linux: fix string formatting of Firefox configs | [e775d68](https://github.com/undergroundwires/privacy.sexy/commit/e775d68a9b4a5f9e893ff0e3500dade036185193) +* win: improve file delete | [e72c1c1](https://github.com/undergroundwires/privacy.sexy/commit/e72c1c13ea2d73ebfc7a8da5a21254fdfc0e5b59) +* win: change system app removal to hard delete #260 | [77123d8](https://github.com/undergroundwires/privacy.sexy/commit/77123d8c929d23676a9cb21d7b697703fd1b6e82) +* Improve UI performance by optimizing reactivity | [4995e49](https://github.com/undergroundwires/privacy.sexy/commit/4995e49c469211404dac9fcb79b75eb121f80bce) +* Migrate to Vue 3.0 #230 | [ca81f68](https://github.com/undergroundwires/privacy.sexy/commit/ca81f68ff1c3bbe5b22981096ae9220b0b5851c7) +* win, linux: unify & improve Firefox clean-up #273 | [0466b86](https://github.com/undergroundwires/privacy.sexy/commit/0466b86f1013341c966a9bbf6513990337b16598) +* win: fix store revert for multiple installs #260 | [5bb13e3](https://github.com/undergroundwires/privacy.sexy/commit/5bb13e34f8de2e2a7ba943ff72b12c0569435e62) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.5...0.12.6) + +## 0.12.5 (2023-10-13) + +* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16) +* Add SAST security checks with SECURITY.md #178 | [3e5239f](https://github.com/undergroundwires/privacy.sexy/commit/3e5239f7d35e57749c01adf3dbbcd365aebb39c8) +* Add Scoop download instructions #174 | [cf55ca9](https://github.com/undergroundwires/privacy.sexy/commit/cf55ca9e28b064fa7a516077a9da23e3a8e3f534) +* win: fix and improve temp dir cleanup #176, #89 | [d457504](https://github.com/undergroundwires/privacy.sexy/commit/d45750428cca010daf2721b33a8ae3a01b28813b) +* win, linux: improve VSCode setting robustness #196 | [e8a52f7](https://github.com/undergroundwires/privacy.sexy/commit/e8a52f717dc799b34ceeb1c27c2b8219391dff6a) +* linux: fix obsolete Firefox DPI script #239 | [e5f6edf](https://github.com/undergroundwires/privacy.sexy/commit/e5f6edf405bcec7c29ea4d7932d1910620fa15f8) +* win: add removal of Edge assocations #64 | [888c916](https://github.com/undergroundwires/privacy.sexy/commit/888c9166fc66a2094137fa8be739cc21bafef5f6) +* win: improve Edge & OneDrive shortcut removal #73 | [8501495](https://github.com/undergroundwires/privacy.sexy/commit/8501495c170af61913288a63dbd369db5bbc5003) +* win: relocate and document SecHealthUI #190 | [2862951](https://github.com/undergroundwires/privacy.sexy/commit/286295128d0179358e0c6b7b6415d752175a1aed) +* Add developer toolkit UI component | [2147eae](https://github.com/undergroundwires/privacy.sexy/commit/2147eae687b82d05bc43bb4605d9068f148bb92a) +* win: fix and improve network data usage reset #265 | [5e359c2](https://github.com/undergroundwires/privacy.sexy/commit/5e359c2fb82a08e6acf7159b70ca86a8234b359b) +* win: improve app reversion and docs #260 | [a3f11df](https://github.com/undergroundwires/privacy.sexy/commit/a3f11dff187c821a00910c20dac05e285cda9073) +* Fix working directory in CI/CD web release | [698b570](https://github.com/undergroundwires/privacy.sexy/commit/698b570ee6e300d6703015464f4345b5e706f1cb) +* Implement new UI component for icons #230 | [48730bc](https://github.com/undergroundwires/privacy.sexy/commit/48730bca0506120bca4bf3a23545d59f2b1a9009) +* win: fix and improve AppCompat disabling #255 | [bab6316](https://github.com/undergroundwires/privacy.sexy/commit/bab6316e7625230cf4a4cf67c3aca417347db75c) +* win, linux, mac: fix typos and improve naming | [67c3677](https://github.com/undergroundwires/privacy.sexy/commit/67c3677621b201525a813e8a26f07d607176e89b) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.4...0.12.5) + +## 0.12.4 (2023-09-25) + +* win: fix Windows spotlight revert, docs, recommend | [659fea7](https://github.com/undergroundwires/privacy.sexy/commit/659fea7afcabcd0ea273cfdcc8c4bae190c126f3) +* win: fix Edge telemetry disabling for v116+ #242 | [6d301f9](https://github.com/undergroundwires/privacy.sexy/commit/6d301f99616ed49975876803d0098eafe4d3cb2e) +* win: fix, improve disabling automatic updates #252 | [6e9b65d](https://github.com/undergroundwires/privacy.sexy/commit/6e9b65d8b1b481c1471dde90876c37838b4ac4e5) +* win: refactor `update.mode` key for VSCode #215 | [c27172c](https://github.com/undergroundwires/privacy.sexy/commit/c27172c32e7c316b7cb0f44cab611eed89ca034e) +* Fix wrong action path in website CI deployment | [a1f2497](https://github.com/undergroundwires/privacy.sexy/commit/a1f24973813ccbdd7e1f06c64e1912a991a6bb64) +* Fix compiler bug with nested optional arguments | [53222fd](https://github.com/undergroundwires/privacy.sexy/commit/53222fd83c2846089746a217482195806f960d18) +* Fix no spacing after lists in documentation text | [f810ed0](https://github.com/undergroundwires/privacy.sexy/commit/f810ed0c147c2a46cae3b70b635ed81128646fff) +* Rewrite tooltip UI for efficiency and Vue 3.0 #230 | [8b930fc](https://github.com/undergroundwires/privacy.sexy/commit/8b930fc57c8ee6691ed6165bcb27d97e64a1a0c0) +* win: fix uninstallation of newer Edge #236 | [60dde11](https://github.com/undergroundwires/privacy.sexy/commit/60dde11311a2409537f5965f370b0daaaec53339) +* win: fix delivery optimization side-effects #173 | [203daeb](https://github.com/undergroundwires/privacy.sexy/commit/203daeb4a2fca0a0295cbc2a736394f9f87725e6) +* win: fix Defender scan artifacts removal #246 | [cb21a97](https://github.com/undergroundwires/privacy.sexy/commit/cb21a970b6b867e1476a5eb8a72b9a7fdd53a744) +* Fix outdated and broken links in README #161 | [0303ef2](https://github.com/undergroundwires/privacy.sexy/commit/0303ef2fd98b36306523e2a0c5f5ae812a4c6c99) +* Fix loss of tree node state when switching views | [8f188ac](https://github.com/undergroundwires/privacy.sexy/commit/8f188acd3c2d93e40c89569c74bc5cff992f0052) +* Fix slow appearance of nodes on tree view | [bd2082e](https://github.com/undergroundwires/privacy.sexy/commit/bd2082e8c574db065bb4462f30ea3ace2cb028cb) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.3...0.12.4) + +## 0.12.3 (2023-09-09) + +* linux: use user.js over prefs.js for Firefox #232 | [dae6d11](https://github.com/undergroundwires/privacy.sexy/commit/dae6d114daab6857d773071211eb57619b136281) +* win: fix typo in Defender retention script #213 | [35be05d](https://github.com/undergroundwires/privacy.sexy/commit/35be05df2094ea8bba4ee4725e6fa4956a79493d) +* Improve desktop runtime execution tests | [ad0576a](https://github.com/undergroundwires/privacy.sexy/commit/ad0576a752f8fd6ea2f917a59173fe61f9951246) +* Fix Windows artifact naming in desktop packaging | [f4d86fc](https://github.com/undergroundwires/privacy.sexy/commit/f4d86fccfd0e73e94c8c6e400a33514900bc5abe) +* Refactor and improve external URL checks | [19e42c9](https://github.com/undergroundwires/privacy.sexy/commit/19e42c9c52a18c813ded4265e687e01032cdd4c8) +* Fix memory leaks via auto-unsubscribing and DI | [eb096d0](https://github.com/undergroundwires/privacy.sexy/commit/eb096d07e276e1b4c8040220c47f186d02841e14) +* Refactor build configs and improve CI/CD checks | [0a2a1a0](https://github.com/undergroundwires/privacy.sexy/commit/0a2a1a026b0efb29624be82b06536c518c1ea439) +* Introduce retry mechanism for npm install in CI/CD | [4beb1bb](https://github.com/undergroundwires/privacy.sexy/commit/4beb1bb5748a60886210187ca3cdc7f4b41067c0) +* win: fix disable recent apps revert #211, #248 | [4ce327e](https://github.com/undergroundwires/privacy.sexy/commit/4ce327eb6af542ed2916d649553e5e1ba5833882) +* Change license to AGPLv3 | [821cc62](https://github.com/undergroundwires/privacy.sexy/commit/821cc62c4c8347cb76d041f82f574754e4d948c5) +* Introduce new TreeView UI component | [65f121c](https://github.com/undergroundwires/privacy.sexy/commit/65f121c451af87315e1c91df4198562e0445b2c2) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.2...0.12.3) + +## 0.12.2 (2023-08-25) + +* Add automated checks for desktop app runtime #233 | [04b3133](https://github.com/undergroundwires/privacy.sexy/commit/04b3133500485d0d278a81a177a1677134131405) +* win: fix automatic updates revert #234 | [0873769](https://github.com/undergroundwires/privacy.sexy/commit/08737698c2283bdf535d1611a730031ebfc7c0df) +* Migrate unit/integration tests to Vitest with Vite | [5f11c8d](https://github.com/undergroundwires/privacy.sexy/commit/5f11c8d98f782dd7c77f27649a1685fb7bd06e13) +* Remove Vue ESLint plugin for Vite compatibility | [6e40edd](https://github.com/undergroundwires/privacy.sexy/commit/6e40edd3f8a063c1b7482c27d8368e14c2fbcfbf) +* Migrate web builds from Vue CLI to Vite | [7365905](https://github.com/undergroundwires/privacy.sexy/commit/736590558be51a09435bb87e78b6655e8533bc2e) +* Migrate Cypress (E2E) tests to Vite and TypeScript | [ec98d84](https://github.com/undergroundwires/privacy.sexy/commit/ec98d8417f779fa818ccdda6bb90f521e1738002) +* Migrate to `electron-vite` and `electron-builder` | [75c9b51](https://github.com/undergroundwires/privacy.sexy/commit/75c9b51bf2d1dc7269adfd7b5ed71acfb5031299) +* Fix searching/filtering bugs #235 | [62f8bfa](https://github.com/undergroundwires/privacy.sexy/commit/62f8bfac2f481c93598fe19a51594769f522d684) +* Improve desktop security by isolating Electron | [e9e0001](https://github.com/undergroundwires/privacy.sexy/commit/e9e0001ef845fa6935c59a4e20a89aac9e71756a) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.1...0.12.2) + +## 0.12.1 (2023-08-17) + +* Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2) +* Improve user privacy with secure outbound links | [3a594ac](https://github.com/undergroundwires/privacy.sexy/commit/3a594ac7fd708dc1e98155ffb9b21acd4e1fcf2d) +* Refactor Vue components using Composition API #230 | [1b9be8f](https://github.com/undergroundwires/privacy.sexy/commit/1b9be8fe2d72d8fb5cf1fed6dcc0b9777171aa98) +* Fix failing security tests | [3bc8da4](https://github.com/undergroundwires/privacy.sexy/commit/3bc8da4cbf1e2bd758dc3fffe4b1e62dc3beb7b3) +* Improve Defender scripts #201 | [061afad](https://github.com/undergroundwires/privacy.sexy/commit/061afad9673a41454c2421c318898f2b4f4cf504) +* Fix failing tests due to failed error logging | [986ba07](https://github.com/undergroundwires/privacy.sexy/commit/986ba078a643de6acbee50fff9cf77494ca7ea7f) +* Implement custom lightweight modal #230 | [9e5491f](https://github.com/undergroundwires/privacy.sexy/commit/9e5491fdbf2d9d40d974f5ad0e879a6d5c6d1e55) +* Refactor usage of tooltips for flexibility | [bc91237](https://github.com/undergroundwires/privacy.sexy/commit/bc91237d7c54bdcd15c5c39a55def50d172bb659) +* Fix revert toggle partial rendering | [39e650c](https://github.com/undergroundwires/privacy.sexy/commit/39e650cf110bee6b1b21d9b2902b36b0e2568d54) +* Increase testability through dependency injection | [ae75059](https://github.com/undergroundwires/privacy.sexy/commit/ae75059cc14db41f55dd2056f528442c7d319dd2) +* Refactor filter (search query) event handling | [6a20d80](https://github.com/undergroundwires/privacy.sexy/commit/6a20d804dc365d22c1248d787f9912271f508eeb) +* Migrate to ES6 modules | [a14929a](https://github.com/undergroundwires/privacy.sexy/commit/a14929a13cc6260b514692d9b4f1cdf5fb85d8b2) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.0...0.12.1) + +## 0.12.0 (2023-08-03) + +* Improve script/category name validation | [b210aad](https://github.com/undergroundwires/privacy.sexy/commit/b210aaddf26629179f77fe19f62f65d8a0ca2b87) +* Improve touch like hover on devices without mouse | [99e24b4](https://github.com/undergroundwires/privacy.sexy/commit/99e24b4134c461c336f6d08f49d193d853325d31) +* Improve click/touch without unintended interaction | [3233d9b](https://github.com/undergroundwires/privacy.sexy/commit/3233d9b8024dd59600edddef6d017e0089f59a9d) +* Align card icons vertically in cards view | [8608072](https://github.com/undergroundwires/privacy.sexy/commit/8608072bfb52d10a843a86d3d89b14e8b9776779) +* Fix broken npm installation and builds | [924b326](https://github.com/undergroundwires/privacy.sexy/commit/924b326244a175428175e0df3a50685ee5ac2ec6) +* Improve documentation support with markdown | [6067bdb](https://github.com/undergroundwires/privacy.sexy/commit/6067bdb24e6729d2249c9685f4f1c514c3167d91) +* win: add more Visual Studio scripts, support 2022 | [df533ad](https://github.com/undergroundwires/privacy.sexy/commit/df533ad3b19cebdf3454895aa2182bd4184e0360) +* win: add script to remove Widgets | [bbc6156](https://github.com/undergroundwires/privacy.sexy/commit/bbc6156281fb3fd4b66c63dec3f765780fafa855) +* Use line endings based on script language #88 | [6b3f465](https://github.com/undergroundwires/privacy.sexy/commit/6b3f4659df0afe1c99a8af6598df44a33c1f863a) +* win: improve OneDrive removal | [58ed7b4](https://github.com/undergroundwires/privacy.sexy/commit/58ed7b456b3cf11774c83c8c1c04db37ef3058c2) +* Use lowercase in script names and search text | [430537f](https://github.com/undergroundwires/privacy.sexy/commit/430537f70411756bbcaae837964c0223f78581e8) +* Improve manual execution instructions | [7d3670c](https://github.com/undergroundwires/privacy.sexy/commit/7d3670c26d0151ddc43303e8ed5e47715f0e0f00) +* Add multiline support for with expression | [e8d06e0](https://github.com/undergroundwires/privacy.sexy/commit/e8d06e0f3e178a69861e0197f9d1cce9af3958f1) +* Break line in inline codes in documentation | [c1c2f29](https://github.com/undergroundwires/privacy.sexy/commit/c1c2f2925fe88ec1f56bf7655b6b9a10aa3ea024) +* win: add script to increase RSA key exchange #165 | [a2e0921](https://github.com/undergroundwires/privacy.sexy/commit/a2e092190d8eb0fc9ceb8533572f04fff52f097b) +* win: add scripts to downloaded file handling #153 | [e7b816d](https://github.com/undergroundwires/privacy.sexy/commit/e7b816d1564afa98c63291f9d7fd6f3fee92f4ec) +* Drop support for dead browsers | [bf0c55f](https://github.com/undergroundwires/privacy.sexy/commit/bf0c55fa60bf2be070678ba27db14baf13fec511) +* Add support for nested templates | [68a5d69](https://github.com/undergroundwires/privacy.sexy/commit/68a5d698a2ce644ce25754016fb9e9bb642e41a7) +* mac: add scripts to configure Parallels Desktop | [64cca1d](https://github.com/undergroundwires/privacy.sexy/commit/64cca1d9b8946b92e21e86deb6db5612570befb1) +* Rework icon with higher quality and new color | [f4a7ca7](https://github.com/undergroundwires/privacy.sexy/commit/f4a7ca76b885b8346d8a9c32e6269eabc2d8139f) +* Relax and improve code validation | [e819993](https://github.com/undergroundwires/privacy.sexy/commit/e8199932b462380741d9f2d8b6b55485ab16af02) +* Add initial Linux support #150 | [c404dfe](https://github.com/undergroundwires/privacy.sexy/commit/c404dfebe2908bb165279f8279f3f5e805b647d7) +* mac: add script to disable personalized ads | [8b374a3](https://github.com/undergroundwires/privacy.sexy/commit/8b374a37b401699d5056bfd6b735b6a26c395ae0) +* Update dependencies and add npm setup script | [5721796](https://github.com/undergroundwires/privacy.sexy/commit/57217963787a8ab0c71d681c6b1673c484c88226) +* Fix macOS desktop build failure in CI | [5901dc5](https://github.com/undergroundwires/privacy.sexy/commit/5901dc5f11dd29be14c2616fc0ceb45196a43224) +* Change subtitle heading to new slogan | [1e80ee1](https://github.com/undergroundwires/privacy.sexy/commit/1e80ee1fb0208d92943619468dc427853cbe8de7) +* win: add new scripts to disable more telemetry | [298b058](https://github.com/undergroundwires/privacy.sexy/commit/298b058e5c89397db6f759b275442ba05499ac8c) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.4...0.12.0) + +## 0.11.4 (2022-03-08) + +* Improve performance of selecting scripts | [8e96c19](https://github.com/undergroundwires/privacy.sexy/commit/8e96c19126aa4cba6418de5ccaa9e2dcf8faab78) +* Fix reverting of Windows NVIDIA telemetry service | [2354f0b](https://github.com/undergroundwires/privacy.sexy/commit/2354f0ba9fed3aa23569b5ea6391a7119fe1ab53) +* Add AirBnb TypeScript overrides for linting | [834ce8c](https://github.com/undergroundwires/privacy.sexy/commit/834ce8cf9e8e46934dfa604526360870d109765b) +* Transpile dependencies for wider browser support | [0e52a99](https://github.com/undergroundwires/privacy.sexy/commit/0e52a99efa2b02d1aba10885a76e03aa6f9be7f8) +* Add more and unify tests for absent object cases | [44d79e2](https://github.com/undergroundwires/privacy.sexy/commit/44d79e2c9a97639bbd188a8fdfd740f1a5a1d6ee) +* Fix Windows DoSvc not being disabled #115 | [43ce834](https://github.com/undergroundwires/privacy.sexy/commit/43ce834750ddf471636d1ece4324d02357947f9f) +* Move stubs from `./stubs` to `./shared/Stubs` | [803ef2b](https://github.com/undergroundwires/privacy.sexy/commit/803ef2bb3eea68306377e40e326c791402998650) +* Improve documentation for developing | [3c3ec80](https://github.com/undergroundwires/privacy.sexy/commit/3c3ec80525b97e8a24db4c44bbf42a7b4e089056) +* Improve documentation for architecture | [1bcc6c8](https://github.com/undergroundwires/privacy.sexy/commit/1bcc6c8b2b923b4d4b1662f990d86b190ce73342) +* Improve existing documentation | [db47440](https://github.com/undergroundwires/privacy.sexy/commit/db47440d470ea6a6e100b620b10d078c01314992) +* Refactor to remove code coupling with Webpack | [5bbbb9c](https://github.com/undergroundwires/privacy.sexy/commit/5bbbb9cecca0a3828036e7fc34dcd66970ce334a) +* Refactor to remove hardcoding of aliases | [481a02a](https://github.com/undergroundwires/privacy.sexy/commit/481a02afd5190eb77a37fa450e50816b2268e99c) +* Document WpnService breaking on Windows 10 #110 | [3785e41](https://github.com/undergroundwires/privacy.sexy/commit/3785e410db461f667a834e0b388d81e4baa028e4) +* Fix error when reverting Windows Defender setting | [956052c](https://github.com/undergroundwires/privacy.sexy/commit/956052c8fff042812fe84fe4d7fa5c579365ff9b) +* Fix Windows 11 being detected as Windows 10 | [d6bc33e](https://github.com/undergroundwires/privacy.sexy/commit/d6bc33ec865d50efc6b8d4ccc2f789edd874fcee) +* Refactor to use version object #59 | [eeb1d5b](https://github.com/undergroundwires/privacy.sexy/commit/eeb1d5b0c40a55675921af3f67f366b2ff658acf) +* Fix Microsoft Defender alert for uninstaller #114 | [112e79a](https://github.com/undergroundwires/privacy.sexy/commit/112e79a64c6153f4ce3b48c27a09639e7647aebc) +* Add donation information | [05a6a84](https://github.com/undergroundwires/privacy.sexy/commit/05a6a84c3739ec900343591ac1f7a9f310cd73f2) +* Bump node environment to 16.x | [242a497](https://github.com/undergroundwires/privacy.sexy/commit/242a497e7debb351da19b20b63a3554f0cca4b5c) +* Bump dependencies to latest | [efd63ff](https://github.com/undergroundwires/privacy.sexy/commit/efd63ff85dea4c9a9c033c54bc1be378742de351) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.3...0.11.4) + +## 0.11.3 (2022-01-05) + +* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd) +* Fix OS desktop detection tests and edge cases | [a8358b8](https://github.com/undergroundwires/privacy.sexy/commit/a8358b8e7a93214f3d22a4488007ded5f623d845) +* Fix clearing Windows product key showing dialog | [9b6636e](https://github.com/undergroundwires/privacy.sexy/commit/9b6636e21a922a4750dc19f4854f8ae679187926) +* Document and unrecommend Cloud Experience Host | [9b5e0b0](https://github.com/undergroundwires/privacy.sexy/commit/9b5e0b0591fee56af52d83334a1f19180a49516f) +* Add initial e2e testing with cypress | [ddd2e70](https://github.com/undergroundwires/privacy.sexy/commit/ddd2e704dbd361cbd219f3dfe644b983ad254095) +* Restructure pipelines and badges | [5a2c263](https://github.com/undergroundwires/privacy.sexy/commit/5a2c263af35b8785e75ead6c43c3f17186dc15c8) +* Fix failing of functions without revert code | [87de017](https://github.com/undergroundwires/privacy.sexy/commit/87de017afd6e08acbd2deea150c6af9c7ee778fc) +* Fix typos in privacy modal #109 | [a1871a2](https://github.com/undergroundwires/privacy.sexy/commit/a1871a2982c9e3192193f836b97b1a6ccda5a2ab) +* Refactor to add readonly interfaces | [c3c5b89](https://github.com/undergroundwires/privacy.sexy/commit/c3c5b897f308f613c252182a02cdd4cfa7150fa3) +* Document and unrecommend AAD app removal #24, #54 | [455084c](https://github.com/undergroundwires/privacy.sexy/commit/455084c17b32d11d046515e8dc1447adf4bea4c3) +* Migrate from TSLint to ESLint | [61b475f](https://github.com/undergroundwires/privacy.sexy/commit/61b475fa8de433cdada2efa7eac197683aacd956) +* Add build checks and improve existing CI/CD checks | [17298f0](https://github.com/undergroundwires/privacy.sexy/commit/17298f0b2c51cb9becc0eb2ffe0d93d6a4c503a6) +* Upgrade to Vue CLI 5 (and webpack 5) | [96265b7](https://github.com/undergroundwires/privacy.sexy/commit/96265b75deafb85978b16460138fb4a814c07cfe) +* Refactor code to comply with ESLint rules | [5b1fbe1](https://github.com/undergroundwires/privacy.sexy/commit/5b1fbe1e2fb1354a5f060f8c8e3794ce756e16a7) +* Fix mutated line endings on Windows | [bd23faa](https://github.com/undergroundwires/privacy.sexy/commit/bd23faa28f6d781581a33d5b780f4b33f7e2cd8b) +* Refactor to improve iterations | [31f7091](https://github.com/undergroundwires/privacy.sexy/commit/31f70913a2f30baf5a9d6690f192e6a63da50114) +* win: unrecommend and document Live ID service #100 | [d11a674](https://github.com/undergroundwires/privacy.sexy/commit/d11a674a3c4ad8f4972a870c2f0977ac53297273) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.2...0.11.3) + +## 0.11.2 (2021-12-03) + +* Fix Windows TrustedInstaller session errors | [20a0071](https://github.com/undergroundwires/privacy.sexy/commit/20a0071c0d3d769a8f31218abdbfc4cafa25c6ff) +* Improve tests for `UserSelection` | [2f90cac](https://github.com/undergroundwires/privacy.sexy/commit/2f90cac52ab9e57615aeec41f9daa842bce770a5) +* Fix disabling/enabling Defender on Windows #104 | [2e08293](https://github.com/undergroundwires/privacy.sexy/commit/2e082932c952b0849ab2b8709ff0c75293b88e95) +* Refactor Saas naming, structure and modules | [bf83c58](https://github.com/undergroundwires/privacy.sexy/commit/bf83c58982ffa178facc6d35e50c7f1eac7ff236) +* Fix Defender features errors in Windows #104 | [d7761ab](https://github.com/undergroundwires/privacy.sexy/commit/d7761ab30e7f1e10a2919c196804d67511d6163a) +* Fix unintendedly inlined Windows scripts | [f2d9881](https://github.com/undergroundwires/privacy.sexy/commit/f2d988138257ff184884e4adc83c39e3bc247e9b) +* Fix Defender error due to non-english Windows #104 | [7c02ffb](https://github.com/undergroundwires/privacy.sexy/commit/7c02ffb6c95382b94f0b05e6f259cc418ec91c93) +* Improve and unify disabling of Windows services | [70cdf38](https://github.com/undergroundwires/privacy.sexy/commit/70cdf3865a0de3214fc9e26fbdada4b0cb413c46) +* Improve Windows defender docs and errors #104 | [d2518b1](https://github.com/undergroundwires/privacy.sexy/commit/d2518b11a7774ec58b9b46a691e2f013855bf0f9) +* Unrecommend and complete Windows Push Notif. #101 | [c65209e](https://github.com/undergroundwires/privacy.sexy/commit/c65209e6a99230f15ace8955e8d5a6f3333d146b) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.1...0.11.2) + +## 0.11.1 (2021-11-04) + +* Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342) +* Fix, document, unrecommend Windows browser cleanup | [5ead1a0](https://github.com/undergroundwires/privacy.sexy/commit/5ead1a087d91948890bc4ae6fea176123f18c285) +* Fix failing URL status checking integration tests | [799fb09](https://github.com/undergroundwires/privacy.sexy/commit/799fb091b8eb06c70ac0c67f2ef5385dce73501f) +* Refactor to remove "Async" function name suffix | [82c43ba](https://github.com/undergroundwires/privacy.sexy/commit/82c43ba2e37fb6e7f62ccd9bec8c5f48575f0613) +* Fix dead URLs and use forks as GitHub references | [97ddc02](https://github.com/undergroundwires/privacy.sexy/commit/97ddc027cb5395a74991cabc1d8c875ee945636d) +* Fix website not loading on Safari | [0db8cc4](https://github.com/undergroundwires/privacy.sexy/commit/0db8cc420655e01cbbed57c4658489b761a15899) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.0...0.11.1) + +## 0.11.0 (2021-10-21) + +* Change "grouping" to "view" | [c0c475f](https://github.com/undergroundwires/privacy.sexy/commit/c0c475ff564b23a4dabcc03ac2909207a8eb61ce) +* Tighten parameter substitution tolerance | [dcccb61](https://github.com/undergroundwires/privacy.sexy/commit/dcccb617813625c224a28242c5b965bb4cd6f189) +* Add optionality for parameters | [6a89c62](https://github.com/undergroundwires/privacy.sexy/commit/6a89c6224bdef5eb96980471f3b3935b9351b197) +* Do not collapse cards on links and code area #88 | [e73c0ad](https://github.com/undergroundwires/privacy.sexy/commit/e73c0ad1bf922b1dd3360fc5aafc3434951fa63c) +* Add scripts to disable, hide and opt-out from Siri | [c92dc1e](https://github.com/undergroundwires/privacy.sexy/commit/c92dc1e25387c65a3a41ca64d2a23cf8131b4c86) +* Improve macOS scripts for cleaning OS logs | [6c3c2e6](https://github.com/undergroundwires/privacy.sexy/commit/6c3c2e6709ec84f8e0411f19c024bab2c7e5753b) +* Add "with" expression for templating #53 | [862914b](https://github.com/undergroundwires/privacy.sexy/commit/862914b06ea9ef74c4b58a9a4164a10a38273638) +* Add support for pipes in templates #53 | [4d7ff7e](https://github.com/undergroundwires/privacy.sexy/commit/4d7ff7edc5a96cc0d99d3c1ca4fdf9bbdace3fd2) +* Bump node environment to 15.x | [2f0321f](https://github.com/undergroundwires/privacy.sexy/commit/2f0321f315ac0da8c713dd50e37032f1de194942) +* Add new UX for optionally downloading updates | [ddf417a](https://github.com/undergroundwires/privacy.sexy/commit/ddf417a16a79551b43576befab0541ea08487969) +* Add pipes to write pretty PowerShell #53 | [5217b0b](https://github.com/undergroundwires/privacy.sexy/commit/5217b0b7587ccfe509ba8adc3a7748b9bae14d7a) +* Improve alignment, padding/margin issues on UI | [c8cb7a5](https://github.com/undergroundwires/privacy.sexy/commit/c8cb7a5c28420557319606da82f56b011e88f470) +* Support disabling per-user services in Windows #16 | [4b23907](https://github.com/undergroundwires/privacy.sexy/commit/4b2390736ac1f9de2d5176b7b07da0e827112f9a) +* Add script to remove Meet Now icon in Windows | [f39ee76](https://github.com/undergroundwires/privacy.sexy/commit/f39ee76c0cda95f54502b19d5c49390fd0f12b5e) +* Add support for more depth in function calls | [20b7d28](https://github.com/undergroundwires/privacy.sexy/commit/20b7d283b02dd751dfbde18ef1fe334c6bf76e2b) +* Increase default screen width on desktop app | [9942df1](https://github.com/undergroundwires/privacy.sexy/commit/9942df16c8334ff041fb92f432a3a29e351c88df) +* Improve disabling of SmartScreen #74 | [0696ed8](https://github.com/undergroundwires/privacy.sexy/commit/0696ed8396e298a358bec17adb91c9145dd90418) +* Remove integration tests from deployments #90 | [37ad26a](https://github.com/undergroundwires/privacy.sexy/commit/37ad26a082851c02497c36e7fce40555b9480e11) +* Use a consistent color system | [b08a6b5](https://github.com/undergroundwires/privacy.sexy/commit/b08a6b5cecf4a53023053695292146edbd24b960) +* Add semi-automatic update support for macOS | [410bcd8](https://github.com/undergroundwires/privacy.sexy/commit/410bcd82445097c29c9fcf0eabf7af9ebcb93c1e) +* Add more ways to disable and clean Defender #74 | [2492f2d](https://github.com/undergroundwires/privacy.sexy/commit/2492f2d8141b3abdf590ccad59680b1f50ecb59e) +* Add privacy over security scripts for macOS #83 | [236a0f6](https://github.com/undergroundwires/privacy.sexy/commit/236a0f6c8241294fc397194cd1b20bdeccbbb50b) +* Change PowerShell double quotes escape | [9aa8166](https://github.com/undergroundwires/privacy.sexy/commit/9aa816689146ee6cd86d8262112677c38651c6bd) +* Change theme colors | [a8031d1](https://github.com/undergroundwires/privacy.sexy/commit/a8031d18d520dd3b0567f7b8cfe2dcd694b65073) +* Improve security hardening for macOS | [e6152fa](https://github.com/undergroundwires/privacy.sexy/commit/e6152fa76f5e7d23b0f79d5dd98713daaecbff90) +* Support disabling of protected services #74 | [ab8bce7](https://github.com/undergroundwires/privacy.sexy/commit/ab8bce768650a10677f0a13b3a9fae93c83802ff) +* Fix minor issues with Defender scripts | [739287a](https://github.com/undergroundwires/privacy.sexy/commit/739287ac71b3f8b04348fc101f1fa06f2d7d86a2) +* Update screenshot | [504fa05](https://github.com/undergroundwires/privacy.sexy/commit/504fa056d7d8b17fc20afd398f9a557495fca7e8) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.3...0.11.0) + +## 0.10.3 (2021-08-27) + +* unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358) +* fix incorrect modification of Desktop folder on ThisPC (#71) | [eb9ac35](https://github.com/undergroundwires/privacy.sexy/commit/eb9ac35a923325cc2c9983ef71c0d904337a58f5) +* add initial integration tests | [49600c5](https://github.com/undergroundwires/privacy.sexy/commit/49600c5f37ca33c1687885fdf02a71ef7d3e6e8c) +* unify usage of sleepAsync and add tests | [36f0805](https://github.com/undergroundwires/privacy.sexy/commit/36f08055909f371fd9cbe3480ea813b963aea22b) +* fix broken URLs and automate broken URL checks #70 | [db62ed7](https://github.com/undergroundwires/privacy.sexy/commit/db62ed7f3ac63e9f2d762eb946060595eb9f5626) +* fix hiding recent files in quick access | [b976b92](https://github.com/undergroundwires/privacy.sexy/commit/b976b920318dba55b32d39f148fdca4f6be3cce3) +* bump dependencies to latest #75, #69 | [0a857aa](https://github.com/undergroundwires/privacy.sexy/commit/0a857aa09ee703d34ad0422bd1731158017a9a58) +* Fix NTP configuration before running the service (#72) | [71e70e5](https://github.com/undergroundwires/privacy.sexy/commit/71e70e50c51249bb10f6203414948b325acc2b2a) +* Fix typo on main page (#82) | [487001a](https://github.com/undergroundwires/privacy.sexy/commit/487001af485fdbb958615d7b52c09c2e386ddaf2) +* Improve issue templates | [f2935e4](https://github.com/undergroundwires/privacy.sexy/commit/f2935e4008f1231ef174f8932290e11715564d20) +* Fix infinitely subscribing to state changes | [ea5f9ec](https://github.com/undergroundwires/privacy.sexy/commit/ea5f9ec27df7cec6ac575e23fef18948d2b8e68a) +* Fix select options being clickable when disabled | [1c6b305](https://github.com/undergroundwires/privacy.sexy/commit/1c6b3057ea6e45125cadf374f20a905712ccdf3c) +* Fix tests for `ParameterSubstitutionParser` | [2a08855](https://github.com/undergroundwires/privacy.sexy/commit/2a08855e5d1bdf74354fd692cbfebd1a48e495ac) +* Fix excessive highlighting on hover | [ec0c972](https://github.com/undergroundwires/privacy.sexy/commit/ec0c972d348ffd5897f115d201031b704875b56a) +* Fix dead URLs | [439cd30](https://github.com/undergroundwires/privacy.sexy/commit/439cd303ff3db96a53664e5f44fefe12b95c5e6c) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.2...0.10.3) + +## 0.10.2 (2021-04-19) + +* in CI/CD, run other tests/check even if one of them fails | [5c43965](https://github.com/undergroundwires/privacy.sexy/commit/5c43965f0bc44f991ada7d3bad68937a80665dc3) +* fix desktop initial window size being bigger than current display size on smaller Linux/Windows screens | [02bdc4c](https://github.com/undergroundwires/privacy.sexy/commit/02bdc4cf0426c452f3fc9af52b819ca9b0757290) +* refactor extra code, duplicates, complexity | [00d8e55](https://github.com/undergroundwires/privacy.sexy/commit/00d8e551db001247fadfb6f6af7a4c5ce19a9e64) +* improve disabling ads and marketing #65 | [040ed27](https://github.com/undergroundwires/privacy.sexy/commit/040ed2701c4a468749901f4c5369b221bc0973c4) +* document breaking behavior in script name #64 | [b1ed3ce](https://github.com/undergroundwires/privacy.sexy/commit/b1ed3ce55f2d003cad1ead23e674aa66d4eb5802) +* add module alias '@tests/' | [60c8061](https://github.com/undergroundwires/privacy.sexy/commit/60c80611eab227791fabb883caf93418cef5fd00) +* document chromium warning for policy changes | [aea04e5](https://github.com/undergroundwires/privacy.sexy/commit/aea04e5f7cd48fbb9b407b68ade75575a6064c82) +* fix script revert activating recommendation level | [a2f1085](https://github.com/undergroundwires/privacy.sexy/commit/a2f10857e2a8debb3ce01f79b0dfbe8649ea9a17) +* fix typo and dead URL in Windows scripts (#70) | [8141a01](https://github.com/undergroundwires/privacy.sexy/commit/8141a01ef798331b4d82f5ca95f7b18df4f6f912) +* fix vue warning for undefined property during render | [b25b8cc](https://github.com/undergroundwires/privacy.sexy/commit/b25b8cc8052655af70b0695c6c3085974d783bb6) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.1...0.10.2) + +## 0.10.1 (2021-03-25) + +* refactor script compilation to make it easy to add new expressions #41 #53 | [646db90](https://github.com/undergroundwires/privacy.sexy/commit/646db9058541cebd0af437554de04fdc6bb63a6e) +* restructure presentation layer | [f3c7413](https://github.com/undergroundwires/privacy.sexy/commit/f3c7413f529be4a00dba7b0ab23904b48ea13a35) +* fix a test where "it" is not used inside "describe" | [1a5f920](https://github.com/undergroundwires/privacy.sexy/commit/1a5f92021f7423cd039f8f5326cd6f99b355c962) +* bump dependencies to latest | [1f515e7](https://github.com/undergroundwires/privacy.sexy/commit/1f515e7be525291c960ccb71db05312db6da53f5) +* fix throttle function not being able to run with argument(s) | [1935db1](https://github.com/undergroundwires/privacy.sexy/commit/1935db10192051401ab00ca2cd767955d0d3b866) +* fix fs module hanging not allowing code to run | [5f527a0](https://github.com/undergroundwires/privacy.sexy/commit/5f527a00cf225d3e74b3f6577d6e2456e919de24) +* refactor all modals to use same dialog component | [6f46cdb](https://github.com/undergroundwires/privacy.sexy/commit/6f46cdb4ed49a8941c6c0dde5c5e2a816c06daef) +* fix safari cleanup scripts that are not working on modern versions | [05932c5](https://github.com/undergroundwires/privacy.sexy/commit/05932c5a36446d551c5bc811165e3295fbe15e3f) +* refactor features to use shared functions #41 | [ac2249f](https://github.com/undergroundwires/privacy.sexy/commit/ac2249f25664827d8a6d2c7ebd659ccf126b0cde) +* increase performance by polyfilling ResizeObserver only if required | [448e378](https://github.com/undergroundwires/privacy.sexy/commit/448e378dc4501f9de69af63634c87d0e5060bf52) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.0...0.10.1) + +## 0.10.0 (2021-03-02) + +* allow functions to call other functions #53 | [7661575](https://github.com/undergroundwires/privacy.sexy/commit/7661575573c6d3e8f4bc28bfa7a124a764c72ef9) +* add option to run script directly in desktop app | [9a6b903](https://github.com/undergroundwires/privacy.sexy/commit/9a6b903b9297802845043fd41115756acd4a145c) +* add script to automatically kill devicecensus process | [c9b91f6](https://github.com/undergroundwires/privacy.sexy/commit/c9b91f6d8f9bd16308b6beda119e7154a985b6cf) +* refactor disabling application experience and document better | [45a3669](https://github.com/undergroundwires/privacy.sexy/commit/45a3669443d82855a52f60524d341c15f380f9e7) +* escape printed characters to prevent command injection #45 | [1260eea](https://github.com/undergroundwires/privacy.sexy/commit/1260eea690e4fa5420e58c9de9f88cc29cb242db) +* move code area to right on bigger screens | [cf39e6d](https://github.com/undergroundwires/privacy.sexy/commit/cf39e6d2541ea547f41d9553c380c54c24c58038) +* more scripts to disable speech recognition and Cortana | [ee43fd9](https://github.com/undergroundwires/privacy.sexy/commit/ee43fd92a019ebd26c13890f9146c5b5bb56afaf) +* add more macos scripts for privacy cleanup | [b0a7d0b](https://github.com/undergroundwires/privacy.sexy/commit/b0a7d0b53b3d8ac144a0241d70c037f460b0c0cc) +* add better error messages to setting vscode settings | [65226f3](https://github.com/undergroundwires/privacy.sexy/commit/65226f3984480d0bc7932fd8d76a328f08308850) +* remove windows scripts for removing non-bloating system apps #55 | [15004ff](https://github.com/undergroundwires/privacy.sexy/commit/15004ff1f1fb85a1d92e11ef695bcb2f37110610) +* remove "preview" disclaimer from macOS | [970221b](https://github.com/undergroundwires/privacy.sexy/commit/970221b996e25fe5b029cbaa78607c9bbc8c3c0e) +* update screenshot | [bd41af4](https://github.com/undergroundwires/privacy.sexy/commit/bd41af466fd135f7dc2f171633e4f60d8547c373) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.2...0.10.0) + +## 0.9.2 (2021-02-13) + +* do not compile with unused locals vuejs/vetur#1063 | [73e0520](https://github.com/undergroundwires/privacy.sexy/commit/73e0520de70cdbaf0ecdc6e9be5e85f003fcfb79) +* fix wrong path for NvTelemtry file in NVIDIA script | [34b8822](https://github.com/undergroundwires/privacy.sexy/commit/34b8822ac821acb47e483e21b57e380551bcf455) +* refactor event handling to consume base class for lifecycling | [f1e21ba](https://github.com/undergroundwires/privacy.sexy/commit/f1e21babbfaac21903594a37e30163bfe3338279) +* make compiler throw if a function call includes an unexpected parameter | [15353d0](https://github.com/undergroundwires/privacy.sexy/commit/15353d0e2513c89ee4ffd9d9c5e9e83ef69b96b6) +* refactor vscode configuration scripts using functions #41 | [67b2d1c](https://github.com/undergroundwires/privacy.sexy/commit/67b2d1c11cd5b131dff93a4437db79d96ed8b3dc) +* refactor state handling to make application available independent of the state | [df273f7](https://github.com/undergroundwires/privacy.sexy/commit/df273f7f635ab156ac51a8dfb3fec66c4979f1c4) +* add test to ensure correct shared functions are being parsed | [d7de420](https://github.com/undergroundwires/privacy.sexy/commit/d7de420d5c91bd9ce64880cd4a4391ad3a0a5401) +* refactor and add tests for NonCollapsingDirective | [5934b17](https://github.com/undergroundwires/privacy.sexy/commit/5934b1728328c3b2ece1597b74dd87477d162175) +* add GitHub issue templates | [daa997b](https://github.com/undergroundwires/privacy.sexy/commit/daa997b21b624d133c6f5e4cd6b70214588f9144) +* correct the typo in application.md (#60) | [575636e](https://github.com/undergroundwires/privacy.sexy/commit/575636e6b728a2bdd1a9bd72c57bbf2752f10887) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.1...0.9.2) + +## 0.9.1 (2021-01-23) + +* in CI/CD, allow publishing to github if release is more than 2 hours old electron-userland/electron-builder#2074 | [cf907d0](https://github.com/undergroundwires/privacy.sexy/commit/cf907d029a6d80682ba78ec887a9c4fab639db51) +* in CI/CD, publish packages for other OSes if single one fails | [4015e2c](https://github.com/undergroundwires/privacy.sexy/commit/4015e2ccd8492e0693365b70fbfe3bd0ac7a6ea2) +* specify desktop publish targets as defaults (may) change | [2316e3f](https://github.com/undergroundwires/privacy.sexy/commit/2316e3fb6867e5d765eafcf675b77f88bd2a0f52) +* fix selection state indicator on cards not showing up | [8b0e47d](https://github.com/undergroundwires/privacy.sexy/commit/8b0e47da38c49cfe2645d7d25970c448ecd200f8) +* transpile using babel for legacy browser support | [7930bef](https://github.com/undergroundwires/privacy.sexy/commit/7930bef48c4e9a4fe0823673958ed8377f5ee533) +* fix node APIs no longer working on desktop nklayman/vue-cli-plugin-electron-builder#610, nklayman/vue-cli-plugin-electron-builder#742 | [d7f9ef1](https://github.com/undergroundwires/privacy.sexy/commit/d7f9ef1cbebe911aa19f29be8c5fa9360550793e) +* improve explanation for selections | [229c13a](https://github.com/undergroundwires/privacy.sexy/commit/229c13a195dee92e4a31731b7b41c319273a16f1) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.0...0.9.1) + +## 0.9.0 (2021-01-15) + +* refactor application.yaml to become an os definition #40 | [f7557bc](https://github.com/undergroundwires/privacy.sexy/commit/f7557bcc0faf44e8395b68c7eb14c5f715f07b92) +* refactor folders to move "/state" (IApplicationState) inside "/context" (IApplicationContext) | [3467241](https://github.com/undergroundwires/privacy.sexy/commit/34672414c3e0757173036e351df0a73c1708ded5) +* add scripts to prevent family safety monitoring | [e14bf2b](https://github.com/undergroundwires/privacy.sexy/commit/e14bf2bfa03efe28ff39942c9891fca605f13eed) +* rework Cortana scripts to remove duplicates, better document and support Windows version 2004/2009 #43 | [7cc161c](https://github.com/undergroundwires/privacy.sexy/commit/7cc161c828a3fa49f6f254e31834a95a502b7aa2) +* rename Application to CategoryCollection #40 | [6fe858d](https://github.com/undergroundwires/privacy.sexy/commit/6fe858d86aeb0f8b6d5ae5c2a5e3c25ff32e5f6f) +* add script to clean previous windows installation #35 | [3455a2c](https://github.com/undergroundwires/privacy.sexy/commit/3455a2ca6ce13f9b0e866d88532a5c3d6de30d4d) +* refactor to allow switching ICategoryCollection context #40 | [2e40605](https://github.com/undergroundwires/privacy.sexy/commit/2e40605d59eb764768457c6af561487e7ff09777) +* fix typo causing uninstalling capabilities to fail #51 | [c299e95](https://github.com/undergroundwires/privacy.sexy/commit/c299e95bc6d588317b69a9efcf5752ff5c9c3926) +* improve uninstalling apps to show errors and exit if taking ownership fails #51 | [72e925f](https://github.com/undergroundwires/privacy.sexy/commit/72e925fb6f908cd58fb50618f29726b3fb54a7f1) +* move application.yaml to collections/windows.yaml #40 | [6b83dcb](https://github.com/undergroundwires/privacy.sexy/commit/6b83dcbf8fa08b4efe9974c7d7a667458f7c595c) +* recommend onedrive removal on strict mode | [663d63b](https://github.com/undergroundwires/privacy.sexy/commit/663d63bde08dd1b0d43ec144c758399cec90ec70) +* document app connector removal and recommend on strict mode | [9d009c4](https://github.com/undergroundwires/privacy.sexy/commit/9d009c40dd411c73c7ae032a78ec51490ecce024) +* recommend removing cortana taskbar icon on standard mode | [7ec889e](https://github.com/undergroundwires/privacy.sexy/commit/7ec889e759df04bba99d3b6c4d0597809bd94058) +* fix unintended null file creation #52 | [2428de2](https://github.com/undergroundwires/privacy.sexy/commit/2428de23ee02de987e7e6ec80ebd67be369d9048) +* add initial macOS support #40 | [8a8b731](https://github.com/undergroundwires/privacy.sexy/commit/8a8b7319d539b31c1d8ad9eaf541762d64f02493) +* add scripts to manage chromium based edge | [86a2b2f](https://github.com/undergroundwires/privacy.sexy/commit/86a2b2fda0b6a2565c550758c7c175fa795926b7) +* update screenshot | [c318bd3](https://github.com/undergroundwires/privacy.sexy/commit/c318bd301a2cbebbf5cdba06c0f18ac291aa4788) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.8.2...0.9.0) + +## 0.8.2 (2020-12-26) + +* replace ampersand in "Movies & TV app" with "and" to prevent batch file from misinterpreting it (#45) | [52d4313](https://github.com/undergroundwires/privacy.sexy/commit/52d4313156d2dcbc508b7271e7d9dfd45723d7bc) +* update dependencies to latest #46 | [d9e44e2](https://github.com/undergroundwires/privacy.sexy/commit/d9e44e25744e5d0aa01b8fc0f0af74c48027aea3) +* fix type assignment error after typescript upgrade | [55f936f](https://github.com/undergroundwires/privacy.sexy/commit/55f936fee9f86757f63fa8952d89711feb247e5b) +* correct typos (#48) | [a744415](https://github.com/undergroundwires/privacy.sexy/commit/a744415eb2ab65ee4f519f863fdd6a43953377bb) +* in ci/cd, do not run security checks if PRs do not change dependencies #48 | [54ba4db](https://github.com/undergroundwires/privacy.sexy/commit/54ba4dbb0bf8f08f9479f8facb2e12c786c1bc51) +* rename app launch tracking tweak to make it more clear #44 | [b3117c2](https://github.com/undergroundwires/privacy.sexy/commit/b3117c27f283c2d5a25fd94021a9f628a272cda6) +* refactor capabilities to use a shared function #41 #47 | [c4ec6a1](https://github.com/undergroundwires/privacy.sexy/commit/c4ec6a1445d2fd5eb923c97b54aee01e272e13a8) +* rename "disable" to "uninstall" for removing capabilities #47 | [8cd3352](https://github.com/undergroundwires/privacy.sexy/commit/8cd3352017f9dc85f8efcd7b450d90f555d3e92e) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.8.1...0.8.2) + +## 0.8.1 (2020-11-16) + +* refactor removing bloatware to use functions #41 | [ffa279f](https://github.com/undergroundwires/privacy.sexy/commit/ffa279f3dfe51db564f0a3859543eb212170e173) +* fix reinstalling store apps by searching appx for all users | [2c5ab3e](https://github.com/undergroundwires/privacy.sexy/commit/2c5ab3ea7da159cfb9fbfbbb7cdd28afbee965ea) +* fix clearing jump lists causing os to break and user pin removal #37 | [92c3dd9](https://github.com/undergroundwires/privacy.sexy/commit/92c3dd923257ac940eab6cbab858698ed55a09b7) +* fix reinstalling store apps by searching appx for all users | [4e72673](https://github.com/undergroundwires/privacy.sexy/commit/4e7267337301fe4a0480ba0603218fca25c2d096) +* refactor unused imports | [45b8dd9](https://github.com/undergroundwires/privacy.sexy/commit/45b8dd972b1edf9e263858c23b27e7a1d2e07077) +* fix not being able to uninstall system apps | [31e08d2](https://github.com/undergroundwires/privacy.sexy/commit/31e08d231d52e2a691400468b7c599c142a29448) +* fix wrong app names caused by wrong Microsoft docs | [e41e40c](https://github.com/undergroundwires/privacy.sexy/commit/e41e40c5bf01e2971d3054fcd3a48f8465a96622) +* unrecommend some system apps and document more | [29c7704](https://github.com/undergroundwires/privacy.sexy/commit/29c7704e0bd38f6e9923cde84accb569b02d2dd6) +* fix not being able to rename paths including brackets | [ad1872e](https://github.com/undergroundwires/privacy.sexy/commit/ad1872e7cd4ad7ef9facf33fadfa8c6a55065dd3) +* fix errors when file already exists | [c26bc20](https://github.com/undergroundwires/privacy.sexy/commit/c26bc209eb167aa71cad10b7f3ea02d0dedd97b0) +* move Microsoft.Appconnector to right category | [b247b12](https://github.com/undergroundwires/privacy.sexy/commit/b247b12c3f009aab4350e33f4779fd193e570050) +* replace deprecated github ::set-env command | [ab7d617](https://github.com/undergroundwires/privacy.sexy/commit/ab7d617886a65fe4f3c2daa929168e5678ccae60) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.8.0...0.8.1) + +## 0.8.0 (2020-11-01) + +* add support for different recommendation levels: strict and standard | [14be301](https://github.com/undergroundwires/privacy.sexy/commit/14be3017c55ed5e0d9bdecb63fcc4e1131e79ab0) +* Add GroupMe and Spotify removal option (#34) | [3785c62](https://github.com/undergroundwires/privacy.sexy/commit/3785c623f837b182d82fa383dfe7709722a67248) +* switch places of download and copy buttons | [50fb290](https://github.com/undergroundwires/privacy.sexy/commit/50fb29038ae19b17ec006093db02cf1e568d53c3) +* change "download" button to "save" on desktop | [07fc555](https://github.com/undergroundwires/privacy.sexy/commit/07fc555324d8bf4fa3594a9701daaa124a873153) +* show icons on cards during indeterminate and fully selected states | [1072505](https://github.com/undergroundwires/privacy.sexy/commit/1072505219edc47d82a91f148d1f310f32869fea) +* add scripts to increase cryptography, enable camera notifications and remove todo app (#36) | [4c68408](https://github.com/undergroundwires/privacy.sexy/commit/4c68408f1ec339dc8d39c7ab044f825a7f7185cb) +* update recommendations to be safer and consistent | [d0019c2](https://github.com/undergroundwires/privacy.sexy/commit/d0019c2c9b1eea620e2e8e02b586903ce62b80e3) +* rework disabling metadata retrieval | [ac70b06](https://github.com/undergroundwires/privacy.sexy/commit/ac70b063b8a15bc528256185792939685be6b36f) +* add all dist folders in gitignore because of files auto-generated by vscode | [1a9db31](https://github.com/undergroundwires/privacy.sexy/commit/1a9db31c7778c3269a71c0bd9665827efda70a02) +* add support for shared functions #41 | [8ce06fa](https://github.com/undergroundwires/privacy.sexy/commit/8ce06facbd54184402a4b1af3c7303e64db85b8a) +* hide scrollbars on code area when not overflowing | [fd28eaa](https://github.com/undergroundwires/privacy.sexy/commit/fd28eaad061c75ea1aa7e0f0d60ea37a7e52f8c4) +* update screenshot | [cfedcd7](https://github.com/undergroundwires/privacy.sexy/commit/cfedcd724cad7708b30c7390a7bca3b6313b6726) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.6...0.8.0) + +## 0.7.6 (2020-10-18) + +* add docs for default0 pointing to github discussion (#30) | [a3fc378](https://github.com/undergroundwires/privacy.sexy/commit/a3fc3782efd346b4c99d2a0b40df2eb0229f5b36) +* add robots.txt to explicitly allow indexing | [4c2f749](https://github.com/undergroundwires/privacy.sexy/commit/4c2f74949b0758d33049bdfa4f0124a28958f8ea) +* add more reversibility | [19a092d](https://github.com/undergroundwires/privacy.sexy/commit/19a092dd31fb3588277f1ab3120b409d98506752) +* refactor to read more from package.json | [784a67a](https://github.com/undergroundwires/privacy.sexy/commit/784a67afff681bc19147d03c947de0e165d97e87) +* simplify "why" section | [77c3d2b](https://github.com/undergroundwires/privacy.sexy/commit/77c3d2bbb8d13db86bb82ed0b5cbeaacfdea3db9) +* update dependencies to latest | [11e0613](https://github.com/undergroundwires/privacy.sexy/commit/11e06131655398db08faeeacff62062e46e0dddd) +* run tests on all operating systems: macos, ubuntu, windows | [d9d7f62](https://github.com/undergroundwires/privacy.sexy/commit/d9d7f62d81d4d8f95104d33211e82641884d711f) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.5...0.7.6) + +## 0.7.5 (2020-09-14) + +* fix reverting (reinstalling) capabilities not working | [939d838](https://github.com/undergroundwires/privacy.sexy/commit/939d838e3535bb1c9b00c8ea9dacb735ae41d700) +* fix tests and checks are not running on PRs | [82d5091](https://github.com/undergroundwires/privacy.sexy/commit/82d509129b4e4a5df4b84786a0d6842a7d26e888) +* fix the recycling bin option (#32) | [15db311](https://github.com/undergroundwires/privacy.sexy/commit/15db3118012a172a2191a2afad57084a65b34642) +* fix rendering issue in older edge/IE | [6efed72](https://github.com/undergroundwires/privacy.sexy/commit/6efed72bf25c2ddf0901caab7f22966ca13cd47a) +* fix pasting in search bar after page load showing no results | [d169434](https://github.com/undergroundwires/privacy.sexy/commit/d1694341578288eeaf8b80caf9296a38d76789f0) +* fix typo | [7dd15ed](https://github.com/undergroundwires/privacy.sexy/commit/7dd15ed06433e0e6583ab0fa46a683ce6554bbea) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.4...0.7.5) + +## 0.7.4 (2020-09-12) + +* fix checked checkbox has blue border | [4ae385b](https://github.com/undergroundwires/privacy.sexy/commit/4ae385b7fcea9014a68442714b7d99e2ee7df7d0) +* fix spectre protection getting single lined #31 | [22b23a9](https://github.com/undergroundwires/privacy.sexy/commit/22b23a9ece446c7f9abd4ede293051eb616ad50a) +* fix missing reg value in denying app access to account | [3c13a9e](https://github.com/undergroundwires/privacy.sexy/commit/3c13a9e837e06e097450b31d7eb0c0e6bf20cefb) +* fix wrong path in clear all firefox user profile settings | [ee66196](https://github.com/undergroundwires/privacy.sexy/commit/ee66196d9a60f27d17ae7f62d02b4f119a47e6e0) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.3...0.7.4) + +## 0.7.3 (2020-09-12) + +* fix vscode settings file override and add more configs | [a0d6172](https://github.com/undergroundwires/privacy.sexy/commit/a0d61728ead04b4455437f85820121a848db9e00) +* fix nvidia tweak error message, categorize and add reversibility | [99a2035](https://github.com/undergroundwires/privacy.sexy/commit/99a2035fdb0766a4dfc2753133eab0d7666516cd) +* improve CPU specific tweaks by conditional platform checks and reversibility | [8df5faf](https://github.com/undergroundwires/privacy.sexy/commit/8df5faf4ef05a49da63973bd0fbb5c5d07d5bd93) +* fix wrong path to the main telemetry file | [de4ac97](https://github.com/undergroundwires/privacy.sexy/commit/de4ac978bdda79573b36d355697b8a028d2c0beb) +* fix naming of firefox cleanup to mention profiles | [3ab48b1](https://github.com/undergroundwires/privacy.sexy/commit/3ab48b1cf5f7f934f07e468ef2318ccee07f530c) +* add reversibility and more scripts to denying app access with better structure | [1d465ee](https://github.com/undergroundwires/privacy.sexy/commit/1d465ee3189d0e5a827453b3f0eb4361efe23770) +* fix comment lines are being detected as duplicate in validation | [b6ccb59](https://github.com/undergroundwires/privacy.sexy/commit/b6ccb5927a20412976a54fd2215eb645092f98a8) +* add more detailed error message | [1f11c39](https://github.com/undergroundwires/privacy.sexy/commit/1f11c39773c12eccfb3efb898b58c2f6f37ab9ca) +* fix typo in a test | [1f19b25](https://github.com/undergroundwires/privacy.sexy/commit/1f19b2528a69383e63e579d2885f01cd804abf6c) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.2...0.7.3) + +## 0.7.2 (2020-09-06) + +* update onesync documentation and do not recommend it as it breaks other apps | [f36d8bf](https://github.com/undergroundwires/privacy.sexy/commit/f36d8bfc7848bb65ac0c641e318a689bf3816ccf) +* add reversibility for biometric disabling and do not recommend it | [db74531](https://github.com/undergroundwires/privacy.sexy/commit/db74531cd4139615c6d595959217d3651f099019) +* fix bad highlighting of selected nodes when using keyboard navigation | [255133a](https://github.com/undergroundwires/privacy.sexy/commit/255133af4dfae40171406648a3e2920f16d71cb3) +* add reversibility to removing bloatware | [c7b2a70](https://github.com/undergroundwires/privacy.sexy/commit/c7b2a703128470a05f12c9c6e8002444def37ef8) +* fix indeterminate state being lost | [1f266c3](https://github.com/undergroundwires/privacy.sexy/commit/1f266c33535f72b69c65985bf2eff27cd2c5a104) +* fix wording in default text in text area | [ca63a09](https://github.com/undergroundwires/privacy.sexy/commit/ca63a0979ef55d07d09d9443e5cea9aa888870a5) +* add best practice suggestion to come back | [f4885b6](https://github.com/undergroundwires/privacy.sexy/commit/f4885b6f1c82752f2143934e336d6d1b1af03015) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.1...0.7.2) + +## 0.7.1 (2020-09-04) + +* fix some browsers (including firefox) downloading the script as a text file | [8c17929](https://github.com/undergroundwires/privacy.sexy/commit/8c17929151f9c4fa5f48564492bbf400ced95eea) +* rename screenshot image file | [b8682a8](https://github.com/undergroundwires/privacy.sexy/commit/b8682a852a14ed6cf49986695d9510b840ac9d3d) +* fix new/changed script higlighting not working on production builds | [8c38dd7](https://github.com/undergroundwires/privacy.sexy/commit/8c38dd73d8c7b77d8d341c0389f4d7229f9b97fd) +* refactor unused imports | [6badfef](https://github.com/undergroundwires/privacy.sexy/commit/6badfef9daace0c5de3fd33652a82bfe22261b11) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.0...0.7.1) + +## 0.7.0 (2020-09-02) + +* [search] better (multilined) message when there are no results | [ec15af0](https://github.com/undergroundwires/privacy.sexy/commit/ec15af01dd020b364c2174fe562fd66227c2320c) +* [search] added clear/close button | [d6fa9a2](https://github.com/undergroundwires/privacy.sexy/commit/d6fa9a2a03c0ebe68b94f0b80cc52b4e200c9213) +* move script generation to /generation | [5df4587](https://github.com/undergroundwires/privacy.sexy/commit/5df458739d076719e350ba194c4f3f772884fcdb) +* add auto-highlighting of selected/updated code | [b789250](https://github.com/undergroundwires/privacy.sexy/commit/b789250cb89e2130b08e1a927df8181cf945dfeb) +* prompt admin priviliges automatically | [f8ba5c4](https://github.com/undergroundwires/privacy.sexy/commit/f8ba5c46e4923d9c35f200f8a08aa6437f7c0ecc) +* add removal of ghost (default0) telemetry user | [c262681](https://github.com/undergroundwires/privacy.sexy/commit/c262681011f39b4412669b6cf233476f676ca550) +* add more windows defender tweaks, categorization and reversibility | [1a34c73](https://github.com/undergroundwires/privacy.sexy/commit/1a34c7374ba56bafa0209bbb55c81b233bb419ed) +* fix NTP script documentation is on wrong place | [3060ebf](https://github.com/undergroundwires/privacy.sexy/commit/3060ebf79cf242370433495cc3e1878b7581b202) +* updated dependencies to latest and audit fixes (#25) | [c628aa9](https://github.com/undergroundwires/privacy.sexy/commit/c628aa9aef8ab7c815661d3c1711e7fbc65c69a2) +* categorize, fix and extend windows log files cleanup | [594a14d](https://github.com/undergroundwires/privacy.sexy/commit/594a14d6ca76cbd27a21877b8c373c1930589ca6) +* add more OneDrive cleanup scripts and categorize them | [978d7d0](https://github.com/undergroundwires/privacy.sexy/commit/978d7d08638dd161082f239ed088b12302f29458) +* add disabling firefox telemetry | [f8b8b4c](https://github.com/undergroundwires/privacy.sexy/commit/f8b8b4c97ab734d5ba7370894b694993924388da) +* add disabling ccleaner telemetry | [018b7e2](https://github.com/undergroundwires/privacy.sexy/commit/018b7e270f207aac926cb12f8069ebfcdce193ce) +* Add disabling of PowerShell 7+ telemetry (#29) | [456e40b](https://github.com/undergroundwires/privacy.sexy/commit/456e40bedf9afcc846f9b13f1ea144cef6115cf6) +* categorize, fix, make scripts reversible in "UI for privacy", "security improvements" and "configure browsers" | [532915b](https://github.com/undergroundwires/privacy.sexy/commit/532915b95da9fecd6b981d91bf489359e4e53caa) +* fix "Configure Defender" being in wrong category #28 | [f709d6a](https://github.com/undergroundwires/privacy.sexy/commit/f709d6a566ed7846b677b383863deda9680a2a9c) +* do not hardcode capability versions and make them reversible | [2afef4e](https://github.com/undergroundwires/privacy.sexy/commit/2afef4ea3d0d3d09aa1fa1eedba8493680bd8f10) +* exclude paint, wordpad and notepad from bloatware removal | [d235dee](https://github.com/undergroundwires/privacy.sexy/commit/d235dee95514a01745aef9479d07f88ffb4b40b8) +* add reversibility on category level | [f51e885](https://github.com/undergroundwires/privacy.sexy/commit/f51e8859eeb32c944126d692cfe03a0320c8b568) +* refactor unused imports & variables | [a23d28f](https://github.com/undergroundwires/privacy.sexy/commit/a23d28f2cfa2d64d45460697cf5ee9d6b5920752) +* fix search (got broken in b789250) with tests and refactorings | [8bbe6eb](https://github.com/undergroundwires/privacy.sexy/commit/8bbe6ebf750f1a1cbab493fb99b5ea91f4e21609) +* update the screenshot to show off highlighting | [b4aacea](https://github.com/undergroundwires/privacy.sexy/commit/b4aacea2a3e0bbcf2d8a79ff67f51c0f19e888a6) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.2...0.7.0) + +## 0.6.2 (2020-08-16) + +* 🐛 fixed disabling error reporting for november 2019 update | [5967347](https://github.com/undergroundwires/privacy.sexy/commit/5967347b80976a519f6f4eb1972a62f3e600df2b) +* 🐛 fixed blank screen and icons on mac | [7fac0fe](https://github.com/undergroundwires/privacy.sexy/commit/7fac0fe79f252e8f9dda4f6f83cd6fa4ba2b539f) +* 🐛 fixed removing onedrive does not delete scheduled tasks | [b6bfc25](https://github.com/undergroundwires/privacy.sexy/commit/b6bfc2572740c0cd46d3bc0058fa767dd5fa862e) +* ⚙️ enhanced tweak to disable for office telemetry | [afc3bfb](https://github.com/undergroundwires/privacy.sexy/commit/afc3bfb3b8896f332c9a196973ded3dce8fd21e4) +* ✨ added script to clear dotnet telemery | [1663bfe](https://github.com/undergroundwires/privacy.sexy/commit/1663bfeac7b6580b1335ca5fcf3587b69c080c72) +* 🐛 fixed changing time server not working | [c69998c](https://github.com/undergroundwires/privacy.sexy/commit/c69998c7cb29ffcf40f0af03b73150736581da69) +* 🔥 removed disabling ClickToRun as it breaks office | [3d3380f](https://github.com/undergroundwires/privacy.sexy/commit/3d3380f27ebeea53f17f49974aaa89300ffaf2dd) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.1...0.6.2) + +## 0.6.1 (2020-08-09) + +* updated documentation | [5963d2b](https://github.com/undergroundwires/privacy.sexy/commit/5963d2bac551083f9d16cce6b851abf0e8b88ce7) +* fixed typo in footer | [5c15a7a](https://github.com/undergroundwires/privacy.sexy/commit/5c15a7a64aaf24578a32713dec491bf494216303) +* more scripts can be reverted | [831c014](https://github.com/undergroundwires/privacy.sexy/commit/831c014f977515454ee6dc664d77a8c434495501) +* moved windows connect now to security & recommended | [6049a2b](https://github.com/undergroundwires/privacy.sexy/commit/6049a2b834d8d17af741f8d8f8b07cd15153b001) +* fixed mac / linux download links | [4c8be45](https://github.com/undergroundwires/privacy.sexy/commit/4c8be45e287b5ea009d6f828f7f327f37850569e) +* tweaks to disable webcam, speech and compatibility telemetry | [a5dbe66](https://github.com/undergroundwires/privacy.sexy/commit/a5dbe66fc175e39397f296ab2ff703e9b0ab4d7c) +* refactorings | [66d4d39](https://github.com/undergroundwires/privacy.sexy/commit/66d4d39d5bf3db305450514c6b6224654dafbfb2) +* fixed removing onedrive does not clean start menu / quick access | [1cc1219](https://github.com/undergroundwires/privacy.sexy/commit/1cc12195a3e9a11c590d3ed64d80299b50f74838) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.0...0.6.1) + +## 0.6.0 (2020-07-26) + +* fixed dead links in documentation | [25ce236](https://github.com/undergroundwires/privacy.sexy/commit/25ce236a7737decaf2eb9b8c29a4c4f34d43f770) +* runs tests on each push on the repository | [73c4268](https://github.com/undergroundwires/privacy.sexy/commit/73c426844a0330718a9ab7de12b61ca05e853323) +* code area now shows "how" before "why" | [4ff4b52](https://github.com/undergroundwires/privacy.sexy/commit/4ff4b52202b1c5dbfe2b80580bbe7d93132ab05c) +* support for desktop versions #20 | [04b9b59](https://github.com/undergroundwires/privacy.sexy/commit/04b9b59e14766ccd251474ad3710baf1f682fd49) +* reworked on footer & removed github icon | [60a5a2a](https://github.com/undergroundwires/privacy.sexy/commit/60a5a2aa4026d384bef9e6a203f1b7514a269c33) +* updated dependencies to latest | [45816a2](https://github.com/undergroundwires/privacy.sexy/commit/45816a2bccb3d11a50e3f2bc19c0a6cc2587deaa) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.5.0...0.6.0) + +## 0.5.0 (2020-07-19) + +* added ability to revert (#21) | [9c063d5](https://github.com/undergroundwires/privacy.sexy/commit/9c063d59defa6297c64f50b49403e8bd10620de9) +* search placeholder shows total scripts | [1d5225d](https://github.com/undergroundwires/privacy.sexy/commit/1d5225de07186f853f4cf7aedd4998f5d00c107a) +* do not collapse card when on "Search" and "Select" | [dd7e141](https://github.com/undergroundwires/privacy.sexy/commit/dd7e1416b4df54bf71b719d4654db88769dc0994) +* opening a card scrolls to its content div | [31d2067](https://github.com/undergroundwires/privacy.sexy/commit/31d2067f076c3159483baec49975617dddbd158d) +* all cards in same line now have same height | [a9f9e90](https://github.com/undergroundwires/privacy.sexy/commit/a9f9e9044385d9aed3b5551fc6c6823e813fd1e5) +* patched loadash vulnerability (#18) | [92a7118](https://github.com/undergroundwires/privacy.sexy/commit/92a7118d1c5013312772e075b9ee5a79c93710b8) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.10...0.5.0) + +## 0.4.10 (2020-07-15) + +* fixed script errors & added tests | [9e722dd](https://github.com/undergroundwires/privacy.sexy/commit/9e722ddfb3825fb29d6298025baaaa033120d017) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.9...0.4.10) + +## 0.4.9 (2020-07-14) + +* disable office telemetry Disassembler0/Win10-Initial-Setup-Script#288 | [53cf595](https://github.com/undergroundwires/privacy.sexy/commit/53cf595e1726ee3de79137fd566978fd512d218f) +* updated to may 2020 update | [909c44d](https://github.com/undergroundwires/privacy.sexy/commit/909c44d72a4a602ee8f27d06b6ec706c1e432ce1) +* simplified docker builds | [f27a287](https://github.com/undergroundwires/privacy.sexy/commit/f27a2871d74e5117fc029be82caef12246e10879) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.8...0.4.9) + +## 0.4.8 (2020-07-11) + +* added more scripts #16 (#17) | [d8552c6](https://github.com/undergroundwires/privacy.sexy/commit/d8552c62ffea13ce62abce836c7dd4980eef6bb9) +* stopping services before disabling #16 | [628c16e](https://github.com/undergroundwires/privacy.sexy/commit/628c16eb952495f5b3f6d794161b355f4b08b819) +* can disable features, capabilities & remove onedrive #16 | [30efbcc](https://github.com/undergroundwires/privacy.sexy/commit/30efbcc621eb83dd5a9c1e66b8f1f5350eb95006) +* updated one more typo (#19) | [d7a1325](https://github.com/undergroundwires/privacy.sexy/commit/d7a1325c0b7665ce712dc411965d00fc1d6fa384) +* more tweaks #16 | [2c4eb78](https://github.com/undergroundwires/privacy.sexy/commit/2c4eb78c3f156cb0d033977cffbe7464697680f5) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.7...0.4.8) + +## 0.4.7 (2020-06-30) + +* removed HKU tweak as all HKU's are changed #10 | [c937af8](https://github.com/undergroundwires/privacy.sexy/commit/c937af8ee7da9aa95131e56abf7bf24800390fe6) +* Fixed types + script in "Clear Windows log files" (#15) | [461a4f1](https://github.com/undergroundwires/privacy.sexy/commit/461a4f122b342369db5cc08c5e30961c64e68cdd) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.6...0.4.7) + +## 0.4.6 (2020-06-16) + +* Fixed Some More Issues (#12) | [52d5713](https://github.com/undergroundwires/privacy.sexy/commit/52d5713a99422cdf900aba819e49e998abac33cc) +* removed failing continuous deployment #14 | [583c566](https://github.com/undergroundwires/privacy.sexy/commit/583c5660d6ac934b845a044e013357aa91f61c15) +* Updated Some Tweaks (#11) | [0fc1845](https://github.com/undergroundwires/privacy.sexy/commit/0fc18459cde57684f00764815062f838f932aed5) +* Updated Some More Tweaks (#13) | [019b838](https://github.com/undergroundwires/privacy.sexy/commit/019b838925e963b7ec052ac76c6faf5650b9eb67) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.5...0.4.6) + +## 0.4.5 (2020-06-13) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.4...0.4.5) + +## 0.4.4 (2020-05-24) + +* fixed close card button not being visible & cleanup | [0d2efe5](https://github.com/undergroundwires/privacy.sexy/commit/0d2efe5b05aa965458b78b8fa43754ce2f4fe11b) +* new footer with privacy policy | [e2ab124](https://github.com/undergroundwires/privacy.sexy/commit/e2ab124fb799f56ada3570fdc911361cb803e889) +* one command to lint everything "npm run lint" | [bb98d20](https://github.com/undergroundwires/privacy.sexy/commit/bb98d20637cbf1d524ebb2973e308773006e3153) +* fix "group by" overflows on smaller screens | [c668a97](https://github.com/undergroundwires/privacy.sexy/commit/c668a97950a1cb7c8bf2a7fd8a72d1101e65e8ce) +* clicking outside of a card closes it | [aab8f21](https://github.com/undergroundwires/privacy.sexy/commit/aab8f21a8d8dbed54798af581e6e1ad9e86a4be1) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.3...0.4.4) + +## 0.4.3 (2020-05-23) + +* removed redundant documentation | [749a140](https://github.com/undergroundwires/privacy.sexy/commit/749a140eb8dba09cb67fec2f8dec937e66e3cff5) +* fixed broke link | [97b7e03](https://github.com/undergroundwires/privacy.sexy/commit/97b7e03233d9718a8df30cb01ce06ca9489a0295) +* simplified heading | [226074c](https://github.com/undergroundwires/privacy.sexy/commit/226074c5342f7463c06fcff1457d352ca30295a3) +* reading version from package.json instead of version file #5 | [691f989](https://github.com/undergroundwires/privacy.sexy/commit/691f989682179016ddcbf55a05cded29155288c9) +* automatically increases patch number #5 | [3e3bc07](https://github.com/undergroundwires/privacy.sexy/commit/3e3bc07576f7c7e74e3e11fc7d197cbb9a9fb8c0) +* using deployment operations from aws-static-site-with-cd | [997be71](https://github.com/undergroundwires/privacy.sexy/commit/997be7113f676888892ffa35566d9ebb58a3e9ea) +* automated using bump-everywhere + more quality checks (#8) | [4a91e8c](https://github.com/undergroundwires/privacy.sexy/commit/4a91e8ccd8a707bc6bea34ee28cff7fa4f66ee2f) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.2...0.4.3) + +## 0.4.2 (2020-02-29) + +* added missing semicolon for masking | [e63ac4a](https://github.com/undergroundwires/privacy.sexy/commit/e63ac4ae67da68243a525af149ff30e5d485b641) +* set font on input | [0c39a06](https://github.com/undergroundwires/privacy.sexy/commit/0c39a06be5e4b0a2031ad5e9f5220dd669afee53) +* shortened all HKEY paths | [802b36b](https://github.com/undergroundwires/privacy.sexy/commit/802b36bdd8dcc1f0a2853fe7da2ea2fccd69a88c) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.1...0.4.2) + +## 0.4.1 (2020-01-11) + +* fixed search bug | [31364bd](https://github.com/undergroundwires/privacy.sexy/commit/31364bdfec503af09ffbb58044a17dfb833fc8d9) +* hide grouping while searching | [92f1a36](https://github.com/undergroundwires/privacy.sexy/commit/92f1a36bcb1e1fe7c90efe8ccd3ede55991e9d9c) +* 👀🔍 showing search queries | [97a7747](https://github.com/undergroundwires/privacy.sexy/commit/97a7747933d2b515cc03ab8243e6a8ae702ef16a) +* more efficient queries with single lowercase | [19813b6](https://github.com/undergroundwires/privacy.sexy/commit/19813b691746d98670823025c460480400e34b6e) +* using right 🔍 input type | [0ce354e](https://github.com/undergroundwires/privacy.sexy/commit/0ce354ea0956391ad3f37b252daac1127bfc601a) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.0...0.4.1) + +## 0.4.0 (2020-01-11) + +* 🔍 support for search | [89862b2](https://github.com/undergroundwires/privacy.sexy/commit/89862b2775703257b9dc2e19fbebde2c0d0fbda0) +* more scripts & better organized | [95baf31](https://github.com/undergroundwires/privacy.sexy/commit/95baf3175b0d2c7df516f7893a96346b94ac8eca) +* refactorings | [e3f82e0](https://github.com/undergroundwires/privacy.sexy/commit/e3f82e069e305f6d94eab335470c8e7b44295dd6) +* more margin for the scripts | [5ea46ec](https://github.com/undergroundwires/privacy.sexy/commit/5ea46ecbf52236953d19f09a8eade08b83e6cd34) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.3.0...0.4.0) + +## 0.3.0 (2020-01-09) + +* added description & more descriptive title | [9957634](https://github.com/undergroundwires/privacy.sexy/commit/99576340b648550149871e2c0fe0b0d8c2dd0d7c) +* allow robots | [eee0e78](https://github.com/undergroundwires/privacy.sexy/commit/eee0e785ec2c5e6bed53d21b4126a57773e35dba) +* removed unused references | [cfd888f](https://github.com/undergroundwires/privacy.sexy/commit/cfd888f3afc5c260a0a4a73f2843b86b9f1df2cd) +* 🚫 disable NVIDIA telemetry | [ab28f4e](https://github.com/undergroundwires/privacy.sexy/commit/ab28f4ed8538d51e1777c86302a63a0cd9c3cb2a) +* backwards compatibility for fonts | [4bc13e1](https://github.com/undergroundwires/privacy.sexy/commit/4bc13e11926a6df77079646499e799742153b4ab) +* added back meta needed for responsiveness | [ed872ef](https://github.com/undergroundwires/privacy.sexy/commit/ed872ef3d9f6c92afc0ce0d06998c60463a8b4e8) +* fancy-font is renamed to main and now used | [6825001](https://github.com/undergroundwires/privacy.sexy/commit/6825001c61426194dc363b96b57a321241f3ba57) +* added support for grouping | [ec6b3c5](https://github.com/undergroundwires/privacy.sexy/commit/ec6b3c54072a77bb4305da1c234db6c649218b88) +* less hyphens as it looks better on mobile | [e0b080a](https://github.com/undergroundwires/privacy.sexy/commit/e0b080af69157f46ba12e2c25e794f5384671b51) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.2.0...0.3.0) + +## 0.2.0 (2020-01-06) + +* added GitHub Actions badge for build & deploy | [a229aca](https://github.com/undergroundwires/privacy.sexy/commit/a229aca68a92bbcd8e8176ac1dd25ce03509e074) +* more badges 📛🏆📜 | [090e831](https://github.com/undergroundwires/privacy.sexy/commit/090e8319091044e53484ba8338510f6fb7c3cb80) +* typo fixes + whitespace refactorings | [e99f210](https://github.com/undergroundwires/privacy.sexy/commit/e99f210c9dcf61a21e445e2a331384b6066f2c98) +* switched content information to "why" section | [beb3c83](https://github.com/undergroundwires/privacy.sexy/commit/beb3c8339f83a224ca66ad8a911a9265ffe7c9c0) +* fixed contribution URL | [7b4277d](https://github.com/undergroundwires/privacy.sexy/commit/7b4277d7706ccf6ba7e4b7b01aa46f8e3852cfc6) +* fixed wrong relation + lighter style | [8d05b03](https://github.com/undergroundwires/privacy.sexy/commit/8d05b03c9f3c9fc015be6615da8c283809712065) +* better URL validation | [aff463d](https://github.com/undergroundwires/privacy.sexy/commit/aff463dd64fecff92a786fcba88621dff6b1cf73) +* refactoring to new function | [c646c10](https://github.com/undergroundwires/privacy.sexy/commit/c646c102730481c3f4648eb714dc0a84ce35b13c) +* optimized find queries & refactorings | [d38f6cd](https://github.com/undergroundwires/privacy.sexy/commit/d38f6cd6a8b33e11df854c7abea05974dc04d4ce) +* 🎨 styled no JS error | [c359f1d](https://github.com/undergroundwires/privacy.sexy/commit/c359f1d89c6874b3cc94154b993e33f58bd32268) +* simplified finding duplicates | [57037aa](https://github.com/undergroundwires/privacy.sexy/commit/57037aaefcc0e80f0f4719cea89568490a731028) +* fixed maintainability badge URL | [aaea47e](https://github.com/undergroundwires/privacy.sexy/commit/aaea47e7d15fe41dea26968db0107a0c53d108f3) +* fixed wrong line dumps | [5ccc7c5](https://github.com/undergroundwires/privacy.sexy/commit/5ccc7c59528885ae7729197df3dfa00f924a2b3f) +* refactorings in parsing | [2aa3742](https://github.com/undergroundwires/privacy.sexy/commit/2aa3742e30646bf1d1f3779419d161c3fb6c4808) +* using free function | [20020af](https://github.com/undergroundwires/privacy.sexy/commit/20020af7c1d8de13948d8761fd4e7f0affb2badb) +* default selection is now none | [3140cc6](https://github.com/undergroundwires/privacy.sexy/commit/3140cc663b86394d543de90228aa53e6a304d8d9) +* added hyphen lines for longer names | [cced601](https://github.com/undergroundwires/privacy.sexy/commit/cced601d686d550f4225018e5311b7433efbb5ae) +* more descriptive subtitle | [2cf9214](https://github.com/undergroundwires/privacy.sexy/commit/2cf9214b14d9720f747a71b3864ba7a28acf0ff4) +* added footer with version | [10a34fa](https://github.com/undergroundwires/privacy.sexy/commit/10a34fae2f1a219ec52db0c74edb39b46ebd8abc) +* using font variables | [60e6348](https://github.com/undergroundwires/privacy.sexy/commit/60e6348dc8d53f1e81ebdb2ec0e1962aac1e9842) +* code-gen refactorings | [246e753](https://github.com/undergroundwires/privacy.sexy/commit/246e753ddc9dc8bf630e538663584bf3423cc749) +* added text when nothing is chosen | [a7da75d](https://github.com/undergroundwires/privacy.sexy/commit/a7da75d4428090423b692ce45423f5bd300d8442) + +[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.1.0...0.2.0) + +## 0.1.0 (2019-12-31) + +Initial release | [commits](https://github.com/undergroundwires/privacy.sexy/commit/4e7f244190c6ffbf7b20443e3e69cf2402c4268a) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cd9f7dee --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing + +Love your input ❤️! Contributing to this project should be as easy and transparent as possible, whether it's: + +- reporting a bug, +- discussing the current state of the code, +- submitting a fix, +- proposing new features, +- or becoming a maintainer. + +As a small open source project with small community, it can sometimes take a long time to address the issues so please be patient. + +## Pull request process + +Your pull requests are actively welcomed. We collaborate using [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow). + +The steps: + +1. Fork the repository and create your branch from `master`. +2. If you've added code that requires testing, add tests. See [tests.md](./docs/tests.md). +3. If you've done a major change, update the documentation. See [docs/](./docs/). +4. Ensure the test suite passes. See [development.md | Testing](./docs/development.md#testing) for commands. +5. Make sure your code lints.See [development.md | Linting](./docs/development.md#linting) for commands. +6. Issue that pull request! + +**🙏 DO:** + +- Document why (what you're trying to solve) rather than what in the pull request. + +**❗ DON'T:** + +- Do not update the versions, current version is [set by the maintainer](./docs/ci-cd.md#gitops) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere). + +Automated pipelines will run to control your PR and they will publish your code once the maintainer merges your PR. + +📖 You can read more in [ci-cd.md](./docs/ci-cd.md). + +## Extend scripts + +If you're interested in adding new scripts to privacy.sexy: + +1. Read [guidelines for a good script](./docs/script-guidelines.md) +2. Choose one of two ways to contribute: + 1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) requesting the addition of a new script. This allows other contributors to develop and add it for you. This will take longer time. + 2. Submit a pull request with your script. This is the faster route to seeing your script included in the project. Add your scripts to the appropriate OS directory in the [collections](src/application/collections/) (for syntax guidance, see [collection-files.md](docs/collection-files.md)) folder, and follow the [pull request process](#pull-request-process). + +## Commit conventions + +- Adhere to the 50/72 rule: + - Commit titles should not exceed 50 characters. + - Limit description lines to 72 characters, except for code blocks or inline codes. +- Avoid including delta (such as `git diff` information) or a list of changed files in the commit message. This information is redundant as it's already part of the commit. +- Focus on explaining the WHY and HOW of the changes, rather than WHAT changes are. +- Begin the commit message with a concise summary of what the commit accomplishes. +- Use imperative language in the commit title. For example, use "add" instead of "added". +- Commit prefixes: + - Prefix bug fixes with `fix:` or `Fix ...`. + - For commits affecting scripts of specific operating systems: + - Prefix the commit title with an OS-specific tag such as `win:` for Windows scripts, `mac:` for macOS scripts, and `linux:` for Linux scripts. + - Combine prefixes for commits affecting more than one operating system, e.g., `win, mac: ...`. + +## Versioning + +We base versioning on the release's content rather than strictly following semantic versioning. + +There are two main types of releases: + +1. **Patch Releases:** These focus on minor UI improvements, bug fixes, refactorings, dependency updates, and documentation updates. For scripts, they involve adjusting recommendation levels, enhancing functionality, and dividing scripts for more precise control. Patch releases may ship minor feature additions if they are essential for fixing a bug. For these updates, we increment the patch number in the `MAJOR.MINOR.PATCH`. + +2. **Feature Releases:** These releases bring significant updates that change how users interact with privacy.sexy. They include major UI enhancements, the introduction of new scripts, and features. For these updates, we increment the minor number in the `MAJOR.MINOR.PATCH`. + +Maintainers tag specific commits with a version number to trigger a release, and [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) automates the release process including updating version numbers throughout the project. + +## Refactoring + +Opportunistic refactoring is welcome. If you're adding a feature or fixing a bug, feel free to also clean up and optimize the related code. Your contributions should leave the code in a better state than when you found it. + +## License + +By contributing to this project, you agree that your contributions are licensed under the [GNU Affero General Public License](./LICENSE) as currently specified. Additionally, you expressly consent to the project maintainers having full authority to modify the licensing terms or relicense your contributions under different terms in the future. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0b451f51 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# Build +FROM node:lts-alpine AS build-stage +WORKDIR /app +COPY . . +RUN npm run install-deps +RUN npm run build \ + && npm run check:verify-build-artifacts -- --web +RUN mkdir /dist \ + && dist_directory=$(node 'scripts/print-dist-dir.js' --web) \ + && cp -a "${dist_directory}/." '/dist' + +# Production stage +FROM nginx:stable-alpine AS production-stage +COPY --from=build-stage /dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..be3f7b28 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 00000000..907f6857 --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +# privacy.sexy — Privacy is sexy + +> Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy. + + +

+ + donation badge + + + contributions are welcome + + + Maintainability + + +
+ + Unit tests status + + + Integration tests status + + + E2E tests status + + +
+ + Status of dependency security checks + + + Status of Static Analysis Security Testing (SAST) + + +
+ + Status of quality checks + + + Status of build checks + + + Status of runtime error checks for the desktop application + + + Status of script checks + + + Status of external URL checks + + +
+ + Git release status + + + Site release status + + + Desktop application release status + + +
+ + Auto-versioned by bump-everywhere + +

+ + +## Get started + +- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy). +- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.6/privacy.sexy-Setup-0.13.6.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.6/privacy.sexy-0.13.6.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.6/privacy.sexy-0.13.6.AppImage). For more options, see [here](#additional-install-options). + +See also: + +- [Desktop vs. Web Features](./docs/desktop/desktop-vs-web-features.md): Differences and unique aspects of desktop and web versions. +- [System Requirements](./docs/desktop/system-requirements.md): Hardware and software requirements for the desktop version. + +💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security. + +[![privacy.sexy application](img/screenshot.png?raw=true )](https://privacy.sexy) + +## Features + +- **Rich**: Hundreds of scripts that aims to give you control of your data. +- **Free**: Both free as in "beer" and free as in "speech". +- **Transparent**. Have full visibility into what the tweaks do as you enable them. +- **Reversible**. Revert if something feels wrong. +- **Accessible**. No need to run any compiled software on your computer with web version. +- **Secure**: Security is a top priority at privacy.sexy with [comprehensive safeguards](./SECURITY.md#security-practices) in place. +- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere). +- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features. +- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md). +- **Portable and simple**. Every script is independently executable without cross-dependencies. + +## Support + +**Sponsor 💕**. Consider sponsoring on [GitHub Sponsors](https://github.com/sponsors/undergroundwires), or you can donate using [other ways such as crypto or a coffee](https://undergroundwires.dev/donate). + +**Star 🤩**. Feel free to give it a star ⭐ . + +**Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts). + +## Additional Install Options + +- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions. +- Other unofficial channels (not maintained by privacy.sexy) for Windows include: + - [Scoop 🥄](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) (latest version): + + ```powershell + scoop bucket add extras + scoop install privacy.sexy + ``` + + - [winget 🪟](https://winget.run/pkg/undergroundwires/privacy.sexy) (may be outdated): + + ```powershell + winget install -e --id undergroundwires.privacy.sexy + ``` + + With winget, updates require manual submission; the auto-update feature within privacy.sexy will notify you of new releases post-installation. + +## Development + +Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment. + +Check [architecture.md](./docs/architecture.md) for an overview of design and how different parts and layers work together. You can refer to [application.md](./docs/application.md) for a closer look at application layer codebase and [presentation.md](./docs/presentation.md) for code related to GUI layer. [collection-files.md](./docs/collection-files.md) explains the YAML files that are the core of the application and [templating.md](./docs/templating.md) documents how to use templating language in those files. In [ci-cd.md](./docs/ci-cd.md), you can read more about the pipelines that automates maintenance tasks and ensures you get what see. + +[docs/](./docs/) folder includes all other documentation. + +## Security + +Security is a top priority at privacy.sexy. +An extensive commitment to security verification ensures this priority. +For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md). + +## Supporters + +[![Supporters appreciation banner showing the supporters](https://undergroundwires.dev/img/supporters.jpg)](https://undergroundwires.dev/supporters) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..1ce2efea --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,77 @@ +# Security Policy + +Security is a top priority at privacy.sexy. +Please report any discovered vulnerabilities responsibly. + +## Reporting a Vulnerability + +Efforts to responsibly disclose findings are greatly appreciated. To report a security vulnerability, follow these steps: + +- For general vulnerabilities, [open an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) using the bug report template. +- For sensitive matters, [contact the developer directly](https://undergroundwires.dev). + +## Security Report Handling + +Upon receiving a security report, the process involves: + +- Confirming the report and identifying affected components. +- Assessing the impact and severity of the issue. +- Fixing the vulnerability and planning a release to address it. +- Keeping the reporter informed about progress. + +## Security Practices + +### Application Security + +privacy.sexy adopts a defense in depth strategy to protect users on multiple layers: + +- **Link Protection:** + privacy.sexy ensures each external link has special attributes for your privacy and security. + These attributes block the new site from accessing the privacy.sexy page, increasing your online safety and privacy. +- **Content Security Policies (CSP):** + privacy.sexy actively follows security guidelines from the Open Web Application Security Project (OWASP) at strictest level. + This approach protects against attacks like Cross Site Scripting (XSS) and data injection. +- **Host System Access Control:** + The desktop application segregates and isolates code sections based on their access levels through sandboxing. + This provides a critical defense mechanism, prevents attackers from introducing harmful code into the app, known as injection attacks. +- **Auditing and Transparency:** + The desktop application improves security and transparency by logging application activities and retaining files of executed scripts + This facilitates detailed auditability and effective troubleshooting, contributing to the integrity and reliability of the application. + Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these logs. +- **Privilege Management:** + The desktop application operates without persistent administrative or `sudo` privileges, reinforcing its security posture. It requests + elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This + approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege. +- **Secure Script Execution/Storage:** + - **Antivirus scans:** + Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans. + This step allows confirming that the scripts are secure and safe to use. + - **Tamper protection:** + The application incorporates integrity checks for tamper protection. + If the script file differs from the user's selected script, the application will not execute or save the script, ensuring the processing + of authentic scripts. + This safeguards against any unwanted modifications. + - **Clean-up:** + Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts. + This allows users to maintain their privacy by removing traces of their usage patterns or script preferences. + +### Update Security and Integrity + +privacy.sexy benefits from automated update processes including security tests. Automated deployments from source code ensure immediate and secure updates, mirroring the latest source code. This aligns the deployed application with the expected source code, enhancing transparency and trust. For more details, see [CI/CD Documentation](./docs/ci-cd.md). + +Every desktop update undergoes a thorough verification process. Updates are cryptographically signed to ensure authenticity and integrity, preventing tampered versions from reaching your device. Version checks are conducted to prevent downgrade attacks. + +### Testing + +privacy.sexy's testing approach includes a mix of automated and community-driven tests. +Details on testing practices are available in the [Testing Documentation](./docs/tests.md). + +## Support + +For help or any questions, [submit a GitHub issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose). Addressing security concerns is a priority, and we ensure the necessary support. + +Support privacy.sexy's commitment to security by [making a donation ❤️](https://github.com/sponsors/undergroundwires). Your contributions aid in maintaining and enhancing the project's security features. + +--- + +Active contribution to the safety and security of privacy.sexy is thanked. This collaborative effort keeps the project resilient and trustworthy for all. diff --git a/cypress-dirs.json b/cypress-dirs.json new file mode 100644 index 00000000..153f10c8 --- /dev/null +++ b/cypress-dirs.json @@ -0,0 +1,5 @@ +{ + "base": "tests/e2e", + "videos": "tests/e2e/videos", + "screenshots": "tests/e2e/videos" +} \ No newline at end of file diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 00000000..4b0c04a8 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'cypress'; +import ViteConfig from './vite.config'; +import cypressDirs from './cypress-dirs.json' assert { type: 'json' }; + +export default defineConfig({ + fixturesFolder: `${cypressDirs.base}/fixtures`, + screenshotsFolder: cypressDirs.screenshots, + + video: true, + videosFolder: cypressDirs.videos, + + e2e: { + baseUrl: `http://localhost:${getApplicationPort()}/`, + specPattern: `${cypressDirs.base}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx} + supportFile: `${cypressDirs.base}/support/e2e.ts`, + }, + + /* + Disabling Chrome's web security to allow for faster DOM queries to access DOM earlier than + `cy.get()`. It bypasses the usual same-origin policy constraints + */ + chromeWebSecurity: false, +}); + +function getApplicationPort(): number { + const port = ViteConfig.server?.port; + if (port === undefined) { + throw new Error('Unknown application port'); + } + return port; +} diff --git a/dist-dirs.json b/dist-dirs.json new file mode 100644 index 00000000..76beebac --- /dev/null +++ b/dist-dirs.json @@ -0,0 +1,5 @@ +{ + "electronUnbundled": "dist-electron-unbundled", + "electronBundled": "dist-electron-bundled", + "web": "dist-web" +} diff --git a/docs/application.md b/docs/application.md new file mode 100644 index 00000000..6e2eb4f8 --- /dev/null +++ b/docs/application.md @@ -0,0 +1,45 @@ +# Application + +Application layer is mainly responsible for: + +- creating an event-based and mutable [application state](#application-state), +- [parsing and compiling](#parsing-and-compiling) the [application data](#application-data). + +📖 Refer to [architecture.md | Layered Application](./architecture.md#layered-application) to read more about the layered architecture. + +## Structure + +Application layer code exists in [`/src/application`](./../src/application/) and includes following structure: + +- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md). +- [**`Common/`**](./../src/application/Common/): Contains common functionality in application layer. +- `...`: rest of the application layer source code organized using folders-by-feature structure. + +## Application state + +It uses [state pattern](https://en.wikipedia.org/wiki/State_pattern) with context and state objects. [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) the "Context" of state pattern provides an instance of [`CategoryCollectionState.ts`](./../src/application/Context/State/CategoryCollectionState.ts) (the "State" of the state pattern) for every supported collection. + +Presentation layer uses a singleton (same instance of) [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) throughout the application to ensure consistent state. + +📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [presentation.md | Application State](./presentation.md#application-state) for deeper look into how the presentation layer manages state. + +## Application data + +Application data is collection files using YAML. You can refer to [collection-files.md](./collection-files.md) to read more about the scheme and structure of application data files. You can also check the source code [collection yaml files](./../src/application/collections/) to directly see the application data using that scheme. + +Application layer [parses and compiles](#parsing-and-compiling) application data into [`Application`](./../src/domain/Application.ts)). Once parsed, application layer provides the necessary functionality to presentation layer based on the application data. You can read more about how presentation layer consumes the application data in [presentation.md | Application Data](./presentation.md#application-data). + +Application layer enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) by leveraging the data to the rest of the source code. It makes it easy for community to contribute on the project by using a declarative language used in collection files. + +### Parsing and compiling + +Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts). + +The build tool loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime. + +Application layer compiles templating syntax during parsing to create the end scripts. You can read more about templating syntax in [templating.md](./templating.md) and how application data uses them through functions in [collection-files.md | Function](./collection-files.md#function). + +The steps to extend the templating syntax: + +1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more. +2. Register your in [CompositeExpressionParser](./../src/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts). diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..d0d6c881 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,81 @@ +# Architecture overview + +This repository consists of: + +- A [layered application](#layered-application). +- [AWS infrastructure](#aws-infrastructure) as code and instructions to host the website. +- [GitOps](#gitops) practices for development, maintenance and deployment. + +## Layered application + +Application is + +- powered by **TypeScript**, **Vue.js** and **Electron** 💪, +- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts. + +Application uses highly decoupled models & services in different DDD layers: + +**Application layer** (see [application.md](./application.md)): + +- Coordinates application activities and consumes the domain layer. + +**Presentation layer** (see [presentation.md](./presentation.md)): + +- Handles UI/UX, consumes both the application and domain layers. +- May communicate directly with the infrastructure layer for technical needs, but avoids domain logic. + +**Domain layer**: + +- Serves as the system's core and central truth. +- It should be independent of other layers and encapsulate the core business concepts. + +**Infrastructure layer**: + +- Provides technical implementations. +- Depends on the application and domain layers in terms of interfaces and contracts but should not include business logic. + +![DDD + vue.js](./../img/architecture/app-ddd.drawio.png) + +### Application state + +State handling uses an event-driven subscription model to signal state changes and special functions to register changes. It does not depend on third party packages. + +The presentation layer can read and modify state through the context. State changes trigger events that components can subscribe to for reactivity. + +Each layer treat application layer differently. + +![State](./../img/architecture/app-state.png) + +*[Presentation layer](./presentation.md)*: + +- Each component holds their own state about presentation-related data. +- Components register shared state changes into application state using functions. +- Components listen to shared state changes using event subscriptions. +- 📖 Read more: [presentation.md | Application state](./presentation.md#application-state). + +*[Application layer](./application.md)*: + +- Stores the application-specific state. +- The state it exposed for read with getter functions and set using setter functions, setter functions also fire application events that allows other parts of application and the view in presentation layer to react. +- So state is mutable, and fires related events when mutated. +- 📖 Read more: [application.md | Application state](./application.md#application-state). + +It's comparable with `flux`, `vuex`, and `pinia`. A difference is that mutable application layer state in privacy.sexy is mutable and lies in single "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks). + +## AWS infrastructure + +The web-site runs on serverless AWS infrastructure. Infrastructure is open-source and deployed as code. [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd) project includes the source code. + +[![AWS solution](../img/architecture/aws-solution.png)](https://github.com/undergroundwires/aws-static-site-with-cd) + +The design priorities highest security then minimizing cloud infrastructure costs. + +This project includes [GitHub Actions](../.github/workflows/) to automatically provision the infrastructure with zero-touch and without any "hidden" steps, ensuring everything is open-source and transparent. Git repositories includes all necessary instructions and automation with [GitOps](#gitops) practices. + +## GitOps + +CI/CD pipelines automate operational tasks based on different Git events. [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) enables this automation. + +📖 Read more in [`ci-cd.md`](./ci-cd.md#gitops). + +[![CI/CD using GitHub Actions](../img/architecture/gitops.png)](../.github/workflows/) diff --git a/docs/ci-cd.md b/docs/ci-cd.md new file mode 100644 index 00000000..f85d8a37 --- /dev/null +++ b/docs/ci-cd.md @@ -0,0 +1,45 @@ +# CI/CD overview + +## GitOps + +CI/CD is fully automated using different Git events and GitHub actions. This repository uses [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) to automate versioning, tagging, creation of `CHANGELOG.md` and GitHub releases. A dedicated workflow [release.desktop.yaml](./../.github/workflows/release.desktop.yaml) creates desktop installers and executables and attaches them into GitHub releases. + +Everything that's merged in the master goes directly to production. + +[![CI/CD using GitHub Actions](./../img/architecture/gitops.png)](../.github/workflows/) + +## Pipeline files + +privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code. + +GitHub workflows i.e. pipelines exist in [`/.github/workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] . + +Local GitHub actions are defined in [`/.github/actions/`](./../.github/actions/) and used to reuse same workflow steps. + +## Pipeline types + +We categorize pipelines into different categories. We use these names in convention when naming files and actions, see [naming conventions](#naming-conventions). + +The categories consist of: + +- `tests`: Different types of tests to verify functionality. +- `checks`: Other controls such as vulnerability scans or styling checks. +- `release`: Pipelines used for release of deployment such as building and testing. + +## Naming conventions + +Convention for naming pipeline files: **`..yaml`**. + +**`type`**: + +- Sub-folders do not work for GitHub workflows [1] so we use `.` prefix to organize them. +- See also [pipeline types](#pipeline-types) for list of all usable types. + +**`name`**: + +- We name workflows using kebab-case. +- E.g. file name `tests.unit.yaml`, pipeline file should set the naem as: `name: unit-tests`. +- Kebab-case allows to have better URL references to them. + - [README.md](./../README.md) uses URL references to show status badges for actions. + +[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows diff --git a/docs/collection-files.md b/docs/collection-files.md new file mode 100644 index 00000000..c4878397 --- /dev/null +++ b/docs/collection-files.md @@ -0,0 +1,175 @@ +# Collection files + +privacy.sexy is a data-driven application that reads YAML files. +This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model. The YAML schema [`.schema.yaml`](./../src/application/collections/.schema.yaml) is provided to provide better IDE support and be used in automated validations. + +Related documentation: + +- 📖 [`Collections README`](./../src/application/collections/README.md) includes references to code as documentation. +- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices. + +## Objects + +### `Collection` + +- Defines categories, scripts, and OS-specific details in a tree structure. +- Allows defining common [functions](#function) for code reuse. + +#### `Collection` syntax + +- `os:` *`string`* **(required)** + - Operating system for the collection. + - 📖 See [`OperatingSystem.ts`](./../src/domain/OperatingSystem.ts) for possible values. +- `actions: [` ***[`Category`](#category)*** `, ... ]` **(required)** + - Renders each parent category as cards in the user interface. + - ❗ A [Collection](#collection) must consist of at least one category. +- `functions: [` ***[`Function`](#function)*** `, ... ]` + - Optional for code reuse. +- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)** + - Sets the scripting language for all inline code used within the collection. + +### Executables + +They represent independently executable actions with documentation and reversibility. + +An Executable is a logical entity that can + +- execute once compiled, +- include a `docs` property for documentation. + +It's either [Category](#category) or a [Script](#script). + +#### `Category` + +Represents a logical group of scripts and subcategories. + +##### `Category` syntax + +- `category:` *`string`* **(required)** + - Name of the category. + - ❗ Must be unique throughout the [collection](#collection). +- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#script) `, ... ]` **(required)** + - ❗ Category must consist of at least one subcategory or script. + - Children can be combination of scripts and subcategories. +- `docs`: *`string`* | `[`*`string`*`, ... ]` + - Markdown-formatted documentation related to the category. + +#### `Script` + +Represents an individual tweak. + +Types (like [functions](#function)): + +1. Inline script: + - Direct code. + - ❗ Requires `code` and optional `revertCode`. +2. Caller script: + - Calls other [functions](#function). + - ❗ Requires `call`, but not `code` or `revertCode`. + +📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md). + +##### `Script` syntax + +- `name`: *`string`* **(required)** + - Script name. + - ❗ Must be unique throughout the [Collection](#collection). +- `code`: *`string`* **(conditionally required)** + - Code to execute when the user selects the script. + - 💡 If defined, it's best practice to also define `revertCode`. + - ❗ Cannot co-exist with `call`, define either `code` with optional `revertCode` or `call`. +- `revertCode`: *`string`* + - Reverts changes made by `code`. + - ❗ Cannot co-exist with `call`, define `revertCode` with `code` or `call`. +- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` **(conditionally required)** + - A shared function or sequence of functions to call (called in order). + - ❗ Cannot co-exist with `code` or `revertCode`, define `code` with optional `revertCode` or `call`. +- `docs`: *`string`* | `[`*`string`*`, ... ]` + - Markdown-formatted documentation related to the script. +- `recommend`: *`"standard"`* | *`"strict"`* | *`undefined`* (default: `undefined`) + - Sets recommendation level. + - Application will not recommend the script if `undefined`. + +📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md). + +### `FunctionCall` + +Specifies a function call. It may require providing argument values to its parameters. + +#### `FunctionCall` syntax + +- `function`: *`string`* **(required)** + - Name of the function to call. + - ❗ Function with same name must defined in `functions` property of [Collection](#collection). +- `parameters`: `[` *`parameterName: parameterValue`* `, ... ]` + - Key-value pairs representing function parameters and their corresponding argument values. + - 📖 See [parameter substitution](./templating.md#parameter-substitution) for an example usage. + - 💡 You can use [expressions (templating)](./templating.md#expressions) when providing argument values for parameters. + +### `Function` + +- Enables reusable code in scripts. +- Functions are templates compiled by privacy.sexy and uses special expression expressions. +- A function can be of two different types (like [scripts](#script)): + 1. Inline function: a function with an inline code. + - ❗ Requires `code` and optionally `revertCode`, but not `call`. + 2. Caller function: a function that calls other functions. + - ❗ Requires `call`, but not `code` or `revertCode`. +- 📖 Read about function expressions in [Templating](./templating.md) with [example usages](./templating.md#parameter-substitution). + +#### `Function` syntax + +- `name`: *`string`* **(required)** + - Name of the function that scripts will use. + - ❗ Function names must be unique. + - ❗ Function names must follow camelCase and start with verbs (e.g., `uninstallStoreApp`). +- `parameters`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]` **(conditionally required)** + - Lists parameters used. + - ❗ Required to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions). +- `code`: *`string`* **(conditionally required)** + - Code to execute when the user selects the script. + - 💡 You can use [expressions (templating)](./templating.md#expressions) in its value. + - 💡 If defined, it's best practice to also define `revertCode`. + - ❗ Cannot co-exist with `call`, define either `code` with optional `revertCode` or `call`. +- `revertCode`: *`string`* + - Reverts changes made by `code`. + - 💡 You can use [expressions (templating)](./templating.md#expressions) in its value. + - ❗ Cannot co-exist with `call`, define `revertCode` with `code` or `call`. +- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` **(conditionally required)** + - A shared function or sequence of functions to call (called in order). + - 💡 You can use [expressions (templating)](./templating.md#expressions) in argument values provided for parameters. + - ❗ Cannot co-exist with `code` or `revertCode`, define `code` with optional `revertCode` or `call`. + +### `FunctionParameter` + +- Defines a single parameter that may require an argument value optionally or mandatory. +- A [`FunctionCall`](#functioncall) provides argument values by a caller. + - A caller can be a [Script](#script) or [Function](#function). + +#### `FunctionParameter` syntax + +- `name`: *`string`* **(required)** + - Name of the parameter that the function has. + - ❗ Required for [expressions (templating)](./templating.md#expressions). + - ❗ Must be unique and consists of alphanumeric characters. +- `optional`: *`boolean`* (default: `false`) + - Indicates the caller must provide and argument value for the parameter. + - 💡 If set to `false` i.e. an argument value is not optional then it expects a non-empty value for the variable. + - E.g., in a [`with` expression](./templating.md#with). + - 💡 Set it to `true` if you will use its argument value conditionally; + - Or else set it to `false` for verbosity or do not define it as default value is `false` anyway. + +### `ScriptingDefinition` + +Sets global scripting properties for a [Collection](#collection). + +#### `ScriptingDefinition` syntax + +- `language:` *`string`* **(required)** + - 📖 See [`ScriptingLanguage.ts`](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values. +- `startCode:` *`string`* **(required)** + - Prepends the given code to the generated script file. + - 💡 You can use global variables such as `$homepage`, `$version`, `$date` via [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`. +- `endCode:` *`string`* **(required)** + - Appends to the given code to the generated script file. + - 💡 You can use global variables such as `$homepage`, `$version`, `$date` via [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`. diff --git a/docs/desktop/desktop-vs-web-features.md b/docs/desktop/desktop-vs-web-features.md new file mode 100644 index 00000000..22722491 --- /dev/null +++ b/docs/desktop/desktop-vs-web-features.md @@ -0,0 +1,93 @@ +# Desktop vs. Web Features + +This table outlines the differences between the desktop and web versions of `privacy.sexy`. + +| Feature | Desktop | Web | +| ------- | ------- | --- | +| [Usage without installation](#usage-without-installation) | 🔴 Not available | 🟢 Available | +| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available | +| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available | +| [Logging](#logging) | 🟢 Available | 🔴 Not available | +| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available | +| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available | + +## Feature descriptions + +### Usage without installation + +You can use the web version directly in a browser without installation. +The desktop version requires download and installation. + +> **Note for Linux users:** On Linux, privacy.sexy is available as an `AppImage`, a portable format that doesn't need traditional installation. +> This allows Linux users to use the desktop version without full installation, akin to the web version. + +### Offline usage + +The web version, once loaded, supports offline use. +Desktop version inherently allows offline usage. + +### Auto-updates + +Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./../ci-cd.md). + +The desktop version ensures secure delivery through cryptographic signatures and version checks. + +[Security is a top priority](./../../SECURITY.md#update-security-and-integrity) at privacy.sexy. + +> **Note for macOS users:** On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs. +> Users get notified about updates but might need to complete the installation manually. +> Consider [donating](https://github.com/sponsors/undergroundwires) to help improve this process ❤️. + +### Logging + +The desktop version supports logging of activities to aid in troubleshooting. +This feature is not available in the web version. + +Log file locations vary by operating system: + +- macOS: `$HOME/Library/Logs/privacy.sexy` +- Linux: `$HOME/.config/privacy.sexy/logs` +- Windows: `%APPDATA%\privacy.sexy\logs` + +> 💡 privacy.sexy provides scripts to securely erase these logs. + +### Secure script execution/storage + +The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience. +This direct execution capability isn't available in the web version due to inherent browser restrictions. + +**Script execution history:** + +For enhanced auditability and easier troubleshooting, the desktop version keeps a record of executed scripts in designated directories. +These locations vary based on the operating system: + +- macOS: `$HOME/Library/Application Support/privacy.sexy/runs` +- Linux: `$HOME/.config/privacy.sexy/runs` +- Windows: `%APPDATA%\privacy.sexy\runs` + +> 💡 privacy.sexy provides scripts to securely erase your script execution history. + +**Script antivirus scans:** + +To enhance system protection, the desktop version of privacy.sexy automatically verifies the security of script +execution files by reading them back. +This process triggers antivirus scans to verify that scripts are safe before the execution. + +**Script integrity checks:** + +The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage. +Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them. +If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script. +This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability. + +**Error handling:** + +The desktop version of privacy.sexy features advanced error handling capabilities. +In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes. +It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively. +This proactive error handling and user guidance enhances the application's security and reliability. + +### Native dialogs + +The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs. +These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities. diff --git a/docs/desktop/system-requirements.md b/docs/desktop/system-requirements.md new file mode 100644 index 00000000..2369d00e --- /dev/null +++ b/docs/desktop/system-requirements.md @@ -0,0 +1,36 @@ +# System Requirements for the Desktop Version + +The following system requirements are the official ones for the desktop version. +While we have tested and confirmed these requirements, the application might also work on other +systems or configurations that haven't undergone official testing. + +## Windows + +- **Version:** Windows 10 and later. +- **Processor:** Intel Pentium 4 or later. +- **Architecture:** 64-bit (x86-64), ARM (ARM64). + +> **⚠️ Compatibility Note:** +> ARM version is only compatible with Windows 11 and later. +> It runs non-natively, leading to slower performance due to emulation [1]. + +## macOS + +- **Version:** macOS Catalina (10.15) and later. +- **Architecture:** Intel-based (x86-64), Apple silicon (ARM64). + +## Linux + +- **Version:** Ubuntu 18.04 and later, Fedora 32 and later, and Debian 10 and later. +- **Processor:** Intel Pentium 4 or later. +- **Architecture:** 64-bit (x86-64). + +## References + +System requirements reflect Electron's platform capabilities [2] and Chromium's recommended configurations [3]. + +For details on the build process, see [electron-builder configuration file](./../../electron-builder.cjs). + +[1]: https://web.archive.org/web/20240428082726/https://learn.microsoft.com/en-us/windows/arm/add-arm-support#emulation-on-arm-based-devices-for-x86-or-x64-windows-apps "Add support Arm devices to your Windows app | Microsoft Learn | learn.microsoft.com" +[2]: https://archive.ph/2024.04.28-082958/https://github.com/electron/electron/blob/main/README.md#platform-support "Platform Support | electron/README.md at main · electron/electron · GitHub | github.com" +[3]: https://web.archive.org/web/20240428082945/https://support.google.com/chrome/a/answer/7100626?hl=en "Chrome browser system requirements - Chrome Enterprise and Education Help | support.google.com" diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 00000000..4c4b7d2b --- /dev/null +++ b/docs/development.md @@ -0,0 +1,102 @@ +# Development + +Before your commit, a good practice is to: + +1. [Run unit tests](#testing) +2. [Lint your code](#linting) + +You could run other types of tests as well, but they may take longer time and overkill for your changes. +Automated actions are set up to execute these tests as necessary. +See [ci-cd.md](./ci-cd.md) for more information. + +## Commands + +### Prerequisites + +- Install Node.js: + - Refer to [action.yml](./../.github/actions/setup-node/action.yml) for the minimum required version compatible with the automated workflows. + - 💡 Recommended: Use [`nvm`](https://github.com/nvm-sh/nvm) CLI to install and switch between Node.js versions. +- Install dependencies using `npm install` (or [`npm run install-deps`](#utility-scripts) for more options). +- For Visual Studio Code users, running the configuration script is recommended to optimize the IDE settings, as detailed in [utility scripts](#utility-scripts). + +### Testing + +- Run unit tests: `npm run test:unit` +- Run integration tests: `npm run test:integration` +- Run end-to-end (e2e) tests: + - `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading. + - `npm run test:cy:run`: Run tests on the production build in a headless mode. +- Run checks: + - `npm run check:desktop`: Run runtime checks for packaged desktop applications ([README.md](./../tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/README.md)). + - You can set environment variables active its flags such as `BUILD=true SCREENSHOT=true npm run check:desktop` + - `npm run check:external-urls`: Test whether external URLs used in applications are alive. + +📖 Read more about testing in [tests](./tests.md). + +### Linting + +- Lint all (recommended 💡): `npm run lint` +- Markdown: `npm run lint:md` +- Markdown consistency `npm run lint:md:consistency` +- Markdown relative URLs: `npm run lint:md:relative-urls` +- JavaScript/TypeScript: `npm run lint:eslint` +- Yaml: `npm run lint:yaml` + +### Running + +**Web:** + +- Run in local server: `npm run dev` + - 💡 Meant for local development with features such as hot-reloading. +- Preview production build: `npm run preview` + - Start a local web server that serves the built solution from `./dist`. + - 💡 Run `npm run build` before `npm run preview`. + +**Desktop apps:** + +- `npm run electron:dev`: The command will build the main process and preload scripts source code, and start a dev server for the renderer, and start the Electron app. +- `npm run electron:preview`: The command will build the main process, preload scripts and renderer source code, and start the Electron app to preview. +- `npm run electron:prebuild`: The command will build the main process, preload scripts and renderer source code. Usually before packaging the Electron application, you need to execute this command. +- `npm run electron:build`: Prebuilds the Electron application, packages and publishes it through `electron-builder`. + +**Docker:** + +1. Build: `docker build -t undergroundwires/privacy.sexy:latest .` +2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest` +3. Application should be available at [`http://localhost:8080`](http://localhost:8080) + +### Building + +- Build web application: `npm run build` +- Build desktop application: `npm run electron:build` +- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons` + +### Scripts + +📖 For detailed options and behavior for any of the following scripts, please refer to the script file itself. + +#### Utility scripts + +- [**`npm run install-deps [-- ]`**](../scripts/npm-install.js): + - Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features. + - For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies. +- [**`python3 ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py): + - Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment. +- [**`python3 ./scripts/validate-collections-yaml`**](../scripts/validate-collections-yaml/README.md): + - Validates the syntax and structure of collection YAML files. + +#### Automation scripts + +- [**`node scripts/print-dist-dir.js []`**](../scripts/print-dist-dir.js): + - Determines the absolute path of a distribution directory based on CLI arguments and outputs its absolute path. +- [**`npm run check:verify-build-artifacts [-- ]`**](../scripts/verify-build-artifacts.js): + - Verifies the existence and content of build artifacts. Useful for ensuring that the build process is generating the expected output. +- [**`node scripts/verify-web-server-status.js --url [URL]`**](../scripts/verify-web-server-status.js): + - Checks if a specified server is up with retries and returns an HTTP 200 status code. + +## Recommended extensions + +You should use EditorConfig to follow project style. + +For Visual Studio Code, [`.vscode/extensions.json`](./../.vscode/extensions.json) includes list of recommended extensions. +You can use [VSCode configuration script](#utility-scripts) to automatically install those. diff --git a/docs/presentation.md b/docs/presentation.md new file mode 100644 index 00000000..24386876 --- /dev/null +++ b/docs/presentation.md @@ -0,0 +1,131 @@ +# Presentation layer + +The presentation layer handles UI concerns using Vue as JavaScript framework and Electron to provide desktop functionality. + +It reflects the [application state](./application.md#application-state) and allows user interactions to modify it. Components manage their own local UI state. + +The presentation layer uses an event-driven architecture for bidirectional reactivity between the application state and UI. State change events flow bottom-up to trigger UI updates, while user events flow top-down through components, some ultimately modifying the application state. + +📖 Refer to [architecture.md (Layered Application)](./architecture.md#layered-application) to read more about the layered architecture. + +## Structure + +- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code. + - [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app. + - [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite + - [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins. + - [**`components/`**](./../src/presentation/components/): Contains Vue components, helpers and styles coupled to Vue components. + - [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers. + - [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections). + - [**`/public/`**](../src/presentation/public/): Contains static assets. + - [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite. + - [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts. + - [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles. + - [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.. + - [**`electron/`**](./../src/presentation/electron/): Contains Electron code. + - [`/main/` **`index.ts`**](./../src/presentation/electron/main/index.ts): Main entry for Electron, managing application windows and lifecycle events. + - [`/preload/` **`index.ts`**](./../src/presentation/electron/preload/index.ts): Script executed before the renderer, securing Node.js features for renderer use. + - [**`/shared/`**](./../src/presentation/electron/shared/): Shared logic between different Electron processes. + - [**`/build/`**](./../src/presentation/electron/build/): `electron-builder` build resources directory, [README.md](./../src/presentation/electron/build/README.md). +- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application. +- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications. +- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite. + +## Visual design best-practices + +- **Clickables**: + Add visual clues for clickable items. + It should be as clear as possible that they're interactable at first look without hovering. + They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility. +- **Borders**: + privacy.sexy prefers sharper edges in its design language. +- **Fonts**: + - Use the primary font for regular text and monospace font for code or specific data. + - Use cursive and logo fonts solely for branding. + - Refer to [standardized font size variables](../src/presentation/assets/styles/_typography.scss) for font sizing, avoiding arbitrary `px`, `em`, `rem`, or percentage values. +- **Spacing**: + Use [global spacing variables](../src/presentation/assets/styles/_spacing.scss) for consistent margin, padding, and gap definitions. + This provides uniform spatial distribution and alignment of elements, enhancing visual harmony and making the UI more scalable and maintainable. + +## Application data + +Components (should) use [`UseApplication`](./../src/presentation/components/Shared/Hooks/UseApplication.ts) to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again. + +[Application.ts](../src/domain/Application.ts) is an immutable domain model that represents application state. It includes: + +- available scripts, collections as defined in [collection files](./collection-files.md), +- package information as defined in [`package.json`](./../package.json). + +You can read more about how application layer provides application data to he presentation in [application.md | Application data](./application.md#application-data). + +## Application state + +This project uses a singleton instance of the application state, making it available to all Vue components. + +The decision to not use third-party state management libraries like [`vuex`](https://web.archive.org/web/20230801191617/https://vuex.vuejs.org/) or [`pinia`](https://web.archive.org/web/20230801191743/https://pinia.vuejs.org/) was made to promote code independence and enhance portability. + +Stateful components can mutate and/or react to state changes (e.g., user selection, search queries) in the [ApplicationContext](./../src/application/Context/ApplicationContext.ts). Vue components import [`CollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) to access both the application context and the state. + +[`UseCollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) provides several functionalities including: + +- **Singleton State Instance**: It creates a singleton instance of the state, which is shared across the presentation layer. The singleton instance ensures that there's a single source of truth for the application's state. +- **State Change Callback and Lifecycle Management**: It offers a mechanism to register callbacks, which will be invoked when the state initializes or mutates. It ensures that components unsubscribe from state events when they are no longer in use or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md). +- **State Access and Modification**: It provides functions to read and mutate for accessing and modifying the state, encapsulating the details of these operations. +- **Event Subscription Lifecycle Management**: Includes an `events` member that simplifies state subscription lifecycle events. This ensures that components unsubscribe from state events when they are no longer in use, or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md). + +📖 Refer to [architecture.md | Application State](./architecture.md#application-state) for an overview of event handling and [application.md | Application State](./presentation.md#application-state) for an in-depth understanding of state management in the application layer. + +## Dependency injections + +The presentation layer uses Vue's native dependency injection system to increase testability and decouple components. + +To add a new dependency: + +1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into: + - **Singletons**: Shared across components, instantiated once. + - **Transients**: Factories yielding a new instance on every access. +2. **Provide the dependency**: + Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. + [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies. +3. **Inject the dependency**: Use `injectKey` to inject a dependency. Pass a selector function to `injectKey` that retrieves the appropriate symbol from the provided dependencies. + - Example usage: `injectKey((keys) => keys.useCollectionState)`; + +## Shared UI components + +Shared UI components ensure consistency and streamline front-end development. + +We use homegrown components over third-party solutions or comprehensive UI frameworks like Quasar to maintain portability and easy maintenance. + +Shared components include: + +- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue): Renders modal windows. +- [TooltipWrapper.vue](./../src/presentation/components/Shared/TooltipWrapper.vue): Provides tooltip functionality for improved information accessibility. +- [FlatButton.vue](./../src/presentation/components/Shared/FlatButton.vue): Creates flat-style buttons for a unified and consistent user interface. + +## Desktop builds + +Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages. + +Host system access is strictly controlled. The [`preloader`](./../src/presentation/electron/preload/) isolates logic that interacts with the host system. These functionalities are then securely exposed to the renderer process (Vue application) using context-bridging. [`ApiContextBridge.ts`](./../src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts) handles the configuration of the exposed APIs, ensuring a secure bridge between the Electron and Vue layers. + +## Styles + +### Style location + +- **Global styles**: The [`assets/styles/`](#structure) directory is reserved for styles that have a broader scope, affecting multiple components or entire layouts. They are generic and should not be tightly coupled to a specific component's functionality. +- **Component-specific styles**: Styles closely tied to a particular component's functionality or appearance should reside near the component they are used by. This makes it easier to locate and modify styles when working on a specific component. + +### Sass naming convention + +- Use lowercase for variables/functions/mixins, e.g.: + - Variable: `$variable: value;` + - Function: `@function function() {}` + - Mixin: `@mixin mixin() {}` +- Use - for a phrase/compound word, e.g.: + - Variable: `$some-variable: value;` + - Function: `@function some-function() {}` + - Mixin: `@mixin some-mixin() {}` +- Grouping and name variables from generic to specific, e.g.: + - ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red` + - ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border` + \ No newline at end of file diff --git a/docs/research/README.md b/docs/research/README.md new file mode 100644 index 00000000..320462bc --- /dev/null +++ b/docs/research/README.md @@ -0,0 +1,24 @@ +# Research Documentation + +Welcome to the research section of privacy.sexy. +This area houses in-depth technical research and analyses, serving as a resource for developers, contributors, and technology enthusiasts. + +**Structure:** + +This folder organizes research into topic-specific subdirectories like `windows`, `linux`, etc. +Each contains materials relevant to its subject. + +**Contents:** + +These documents offer comprehensive insights into the respective topics, supporting development and contributions. + +**Contributing:** + +Contributions to our research documentation are welcome. +If your research aligns with privacy.sexy goals, please consider adding it here. +See [`CONTRIBUTING.md`](./../../CONTRIBUTING.md) on more information about how to contribute. + +**Usage:** + +This information is available for educational and research purposes. +We support knowledge sharing and aim to enhance understanding of privacy and security technologies. diff --git a/docs/research/windows/01-windows-10-1909-apps.txt b/docs/research/windows/01-windows-10-1909-apps.txt new file mode 100644 index 00000000..ed885da6 --- /dev/null +++ b/docs/research/windows/01-windows-10-1909-apps.txt @@ -0,0 +1,84 @@ + +Name PublisherId Category NonRemovable +---- ----------- -------- ------------ +1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True +c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True +E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True +F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True +InputApp cw5n1h2txyewy System True +Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True +Microsoft.AccountsControl cw5n1h2txyewy System True +Microsoft.AsyncTextService 8wekyb3d8bbwe System True +Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False +Microsoft.BioEnrollment cw5n1h2txyewy System True +Microsoft.CredDialogHost cw5n1h2txyewy System True +Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned False +Microsoft.ECApp 8wekyb3d8bbwe System True +Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False +Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False +Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.LockApp cw5n1h2txyewy System True +Microsoft.Messaging 8wekyb3d8bbwe Provisioned False +Microsoft.Microsoft3DViewer 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True +Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True +Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False +Microsoft.MixedReality.Portal 8wekyb3d8bbwe Provisioned False +Microsoft.MSPaint 8wekyb3d8bbwe Provisioned False +Microsoft.Office.OneNote 8wekyb3d8bbwe Provisioned False +Microsoft.OneConnect 8wekyb3d8bbwe Provisioned False +Microsoft.People 8wekyb3d8bbwe Provisioned False +Microsoft.PPIProjection cw5n1h2txyewy System True +Microsoft.Print3D 8wekyb3d8bbwe Provisioned False +Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False +Microsoft.SkypeApp kzf8qxf38zg5c Provisioned False +Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False +Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.Wallet 8wekyb3d8bbwe Provisioned False +Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.Win32WebViewHost cw5n1h2txyewy System True +Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True +Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True +Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True +Microsoft.Windows.CapturePicker cw5n1h2txyewy System True +Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True +Microsoft.Windows.Cortana cw5n1h2txyewy System True +Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True +Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True +Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True +Microsoft.Windows.ParentalControls cw5n1h2txyewy System True +Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False +Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True +Microsoft.Windows.SecHealthUI cw5n1h2txyewy System True +Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True +Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True +Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False +microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False +Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False +Microsoft.XboxApp 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGameCallableUI cw5n1h2txyewy System True +Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False +Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False +Windows.CBSPreview cw5n1h2txyewy System True +windows.immersivecontrolpanel cw5n1h2txyewy System True +Windows.PrintDialog cw5n1h2txyewy System True + + diff --git a/docs/research/windows/02-windows-10-20H2-apps.txt b/docs/research/windows/02-windows-10-20H2-apps.txt new file mode 100644 index 00000000..66fa7b1b --- /dev/null +++ b/docs/research/windows/02-windows-10-20H2-apps.txt @@ -0,0 +1,85 @@ + +Name PublisherId Category NonRemovable +---- ----------- -------- ------------ +1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True +c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True +E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True +F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True +Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False +Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True +Microsoft.AccountsControl cw5n1h2txyewy System True +Microsoft.AsyncTextService 8wekyb3d8bbwe System True +Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False +Microsoft.BioEnrollment cw5n1h2txyewy System True +Microsoft.CredDialogHost cw5n1h2txyewy System True +Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned False +Microsoft.ECApp 8wekyb3d8bbwe System True +Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False +Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False +Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.LockApp cw5n1h2txyewy System True +Microsoft.Microsoft3DViewer 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True +Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True +Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False +Microsoft.MixedReality.Portal 8wekyb3d8bbwe Provisioned False +Microsoft.MSPaint 8wekyb3d8bbwe Provisioned False +Microsoft.Office.OneNote 8wekyb3d8bbwe Provisioned False +Microsoft.People 8wekyb3d8bbwe Provisioned False +Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False +Microsoft.SkypeApp kzf8qxf38zg5c Provisioned False +Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False +Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False +Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.Wallet 8wekyb3d8bbwe Provisioned False +Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.Win32WebViewHost cw5n1h2txyewy System True +Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True +Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True +Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True +Microsoft.Windows.CapturePicker cw5n1h2txyewy System True +Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True +Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True +Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True +Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True +Microsoft.Windows.ParentalControls cw5n1h2txyewy System True +Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False +Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True +Microsoft.Windows.Search cw5n1h2txyewy System True +Microsoft.Windows.SecHealthUI cw5n1h2txyewy System True +Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True +Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True +Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False +microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False +Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False +Microsoft.XboxApp 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGameCallableUI cw5n1h2txyewy System True +Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False +Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False +MicrosoftWindows.Client.CBS cw5n1h2txyewy System True +MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True +NcsiUwpApp 8wekyb3d8bbwe System True +Windows.CBSPreview cw5n1h2txyewy System True +windows.immersivecontrolpanel cw5n1h2txyewy System True +Windows.PrintDialog cw5n1h2txyewy System True + + diff --git a/docs/research/windows/03-windows-10-21H2-apps.txt b/docs/research/windows/03-windows-10-21H2-apps.txt new file mode 100644 index 00000000..66fa7b1b --- /dev/null +++ b/docs/research/windows/03-windows-10-21H2-apps.txt @@ -0,0 +1,85 @@ + +Name PublisherId Category NonRemovable +---- ----------- -------- ------------ +1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True +c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True +E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True +F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True +Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False +Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True +Microsoft.AccountsControl cw5n1h2txyewy System True +Microsoft.AsyncTextService 8wekyb3d8bbwe System True +Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False +Microsoft.BioEnrollment cw5n1h2txyewy System True +Microsoft.CredDialogHost cw5n1h2txyewy System True +Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned False +Microsoft.ECApp 8wekyb3d8bbwe System True +Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False +Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False +Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.LockApp cw5n1h2txyewy System True +Microsoft.Microsoft3DViewer 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True +Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True +Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False +Microsoft.MixedReality.Portal 8wekyb3d8bbwe Provisioned False +Microsoft.MSPaint 8wekyb3d8bbwe Provisioned False +Microsoft.Office.OneNote 8wekyb3d8bbwe Provisioned False +Microsoft.People 8wekyb3d8bbwe Provisioned False +Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False +Microsoft.SkypeApp kzf8qxf38zg5c Provisioned False +Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False +Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False +Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.Wallet 8wekyb3d8bbwe Provisioned False +Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.Win32WebViewHost cw5n1h2txyewy System True +Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True +Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True +Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True +Microsoft.Windows.CapturePicker cw5n1h2txyewy System True +Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True +Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True +Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True +Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True +Microsoft.Windows.ParentalControls cw5n1h2txyewy System True +Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False +Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True +Microsoft.Windows.Search cw5n1h2txyewy System True +Microsoft.Windows.SecHealthUI cw5n1h2txyewy System True +Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True +Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True +Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False +microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False +Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False +Microsoft.XboxApp 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGameCallableUI cw5n1h2txyewy System True +Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False +Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False +MicrosoftWindows.Client.CBS cw5n1h2txyewy System True +MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True +NcsiUwpApp 8wekyb3d8bbwe System True +Windows.CBSPreview cw5n1h2txyewy System True +windows.immersivecontrolpanel cw5n1h2txyewy System True +Windows.PrintDialog cw5n1h2txyewy System True + + diff --git a/docs/research/windows/04-windows-10-22H2-apps.txt b/docs/research/windows/04-windows-10-22H2-apps.txt new file mode 100644 index 00000000..66fa7b1b --- /dev/null +++ b/docs/research/windows/04-windows-10-22H2-apps.txt @@ -0,0 +1,85 @@ + +Name PublisherId Category NonRemovable +---- ----------- -------- ------------ +1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True +c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True +E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True +F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True +Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False +Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True +Microsoft.AccountsControl cw5n1h2txyewy System True +Microsoft.AsyncTextService 8wekyb3d8bbwe System True +Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False +Microsoft.BioEnrollment cw5n1h2txyewy System True +Microsoft.CredDialogHost cw5n1h2txyewy System True +Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned False +Microsoft.ECApp 8wekyb3d8bbwe System True +Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False +Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False +Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.LockApp cw5n1h2txyewy System True +Microsoft.Microsoft3DViewer 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True +Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True +Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False +Microsoft.MixedReality.Portal 8wekyb3d8bbwe Provisioned False +Microsoft.MSPaint 8wekyb3d8bbwe Provisioned False +Microsoft.Office.OneNote 8wekyb3d8bbwe Provisioned False +Microsoft.People 8wekyb3d8bbwe Provisioned False +Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False +Microsoft.SkypeApp kzf8qxf38zg5c Provisioned False +Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False +Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False +Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.Wallet 8wekyb3d8bbwe Provisioned False +Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.Win32WebViewHost cw5n1h2txyewy System True +Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True +Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True +Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True +Microsoft.Windows.CapturePicker cw5n1h2txyewy System True +Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True +Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True +Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True +Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True +Microsoft.Windows.ParentalControls cw5n1h2txyewy System True +Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False +Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True +Microsoft.Windows.Search cw5n1h2txyewy System True +Microsoft.Windows.SecHealthUI cw5n1h2txyewy System True +Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True +Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True +Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False +microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False +Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False +Microsoft.XboxApp 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGameCallableUI cw5n1h2txyewy System True +Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False +Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False +MicrosoftWindows.Client.CBS cw5n1h2txyewy System True +MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True +NcsiUwpApp 8wekyb3d8bbwe System True +Windows.CBSPreview cw5n1h2txyewy System True +windows.immersivecontrolpanel cw5n1h2txyewy System True +Windows.PrintDialog cw5n1h2txyewy System True + + diff --git a/docs/research/windows/05-windows-11-21H2-apps.txt b/docs/research/windows/05-windows-11-21H2-apps.txt new file mode 100644 index 00000000..ea22e311 --- /dev/null +++ b/docs/research/windows/05-windows-11-21H2-apps.txt @@ -0,0 +1,88 @@ + +Name PublisherId Category NonRemovable +---- ----------- -------- ------------ +1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True +c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True +E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True +F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True +Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False +Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True +Microsoft.AccountsControl cw5n1h2txyewy System True +Microsoft.AsyncTextService 8wekyb3d8bbwe System True +Microsoft.BingNews 8wekyb3d8bbwe Provisioned False +Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False +Microsoft.BioEnrollment cw5n1h2txyewy System True +Microsoft.CredDialogHost cw5n1h2txyewy System True +Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned True +Microsoft.ECApp 8wekyb3d8bbwe System True +Microsoft.GamingApp 8wekyb3d8bbwe Provisioned False +Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False +Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False +Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.LockApp cw5n1h2txyewy System True +Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True +Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True +Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False +Microsoft.OneDriveSync 8wekyb3d8bbwe Installed False +Microsoft.Paint 8wekyb3d8bbwe Provisioned False +Microsoft.People 8wekyb3d8bbwe Provisioned False +Microsoft.PowerAutomateDesktop 8wekyb3d8bbwe Provisioned False +Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False +Microsoft.SecHealthUI 8wekyb3d8bbwe Provisioned True +Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False +Microsoft.Todos 8wekyb3d8bbwe Provisioned False +Microsoft.UI.Xaml.2.4 8wekyb3d8bbwe Provisioned False +Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False +Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.Win32WebViewHost cw5n1h2txyewy System True +Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True +Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True +Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True +Microsoft.Windows.CapturePicker cw5n1h2txyewy System True +Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True +Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True +Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True +Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True +Microsoft.Windows.ParentalControls cw5n1h2txyewy System True +Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False +Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True +Microsoft.Windows.Search cw5n1h2txyewy System True +Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True +Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True +Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False +microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsNotepad 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsTerminal 8wekyb3d8bbwe Provisioned False +Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGameCallableUI cw5n1h2txyewy System True +Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False +Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False +MicrosoftWindows.Client.CBS cw5n1h2txyewy System True +MicrosoftWindows.Client.WebExperience cw5n1h2txyewy Provisioned False +MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True +NcsiUwpApp 8wekyb3d8bbwe System True +Windows.CBSPreview cw5n1h2txyewy System True +windows.immersivecontrolpanel cw5n1h2txyewy System True +Windows.PrintDialog cw5n1h2txyewy System True + + diff --git a/docs/research/windows/06-windows-11-22H2-apps.txt b/docs/research/windows/06-windows-11-22H2-apps.txt new file mode 100644 index 00000000..66adca21 --- /dev/null +++ b/docs/research/windows/06-windows-11-22H2-apps.txt @@ -0,0 +1,91 @@ + +Name PublisherId Category NonRemovable +---- ----------- -------- ------------ +1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True +c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True +Clipchamp.Clipchamp yxz26nhyzhsrt Provisioned False +E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True +F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True +Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False +Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True +Microsoft.AccountsControl cw5n1h2txyewy System True +Microsoft.AsyncTextService 8wekyb3d8bbwe System True +Microsoft.BingNews 8wekyb3d8bbwe Provisioned False +Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False +Microsoft.BioEnrollment cw5n1h2txyewy System True +Microsoft.CredDialogHost cw5n1h2txyewy System True +Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned True +Microsoft.ECApp 8wekyb3d8bbwe System True +Microsoft.GamingApp 8wekyb3d8bbwe Provisioned False +Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False +Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False +Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.HEVCVideoExtension 8wekyb3d8bbwe Provisioned False +Microsoft.LockApp cw5n1h2txyewy System True +Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True +Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True +Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False +Microsoft.Paint 8wekyb3d8bbwe Provisioned False +Microsoft.People 8wekyb3d8bbwe Provisioned False +Microsoft.PowerAutomateDesktop 8wekyb3d8bbwe Provisioned False +Microsoft.RawImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False +Microsoft.SecHealthUI 8wekyb3d8bbwe Provisioned True +Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False +Microsoft.Todos 8wekyb3d8bbwe Provisioned False +Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False +Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.Win32WebViewHost cw5n1h2txyewy System True +Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True +Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True +Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True +Microsoft.Windows.CapturePicker cw5n1h2txyewy System True +Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True +Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True +Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True +Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True +Microsoft.Windows.ParentalControls cw5n1h2txyewy System True +Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False +Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True +Microsoft.Windows.PrintQueueActionCenter cw5n1h2txyewy System True +Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True +Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True +Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False +microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsNotepad 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsTerminal 8wekyb3d8bbwe Provisioned False +Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGameCallableUI cw5n1h2txyewy System True +Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False +Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False +MicrosoftCorporationII.QuickAssist 8wekyb3d8bbwe Provisioned False +MicrosoftWindows.Client.CBS cw5n1h2txyewy System True +MicrosoftWindows.Client.Core cw5n1h2txyewy System True +MicrosoftWindows.Client.WebExperience cw5n1h2txyewy Provisioned False +MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True +NcsiUwpApp 8wekyb3d8bbwe System True +Windows.CBSPreview cw5n1h2txyewy System True +windows.immersivecontrolpanel cw5n1h2txyewy System True +Windows.PrintDialog cw5n1h2txyewy System True + + diff --git a/docs/research/windows/07-windows-11-23H2-apps.txt b/docs/research/windows/07-windows-11-23H2-apps.txt new file mode 100644 index 00000000..723e7abc --- /dev/null +++ b/docs/research/windows/07-windows-11-23H2-apps.txt @@ -0,0 +1,91 @@ + +Name PublisherId Category NonRemovable +---- ----------- -------- ------------ +1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True +c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True +Clipchamp.Clipchamp yxz26nhyzhsrt Provisioned False +E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True +F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True +Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False +Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True +Microsoft.AccountsControl cw5n1h2txyewy System True +Microsoft.AsyncTextService 8wekyb3d8bbwe System True +Microsoft.BingNews 8wekyb3d8bbwe Provisioned False +Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False +Microsoft.BioEnrollment cw5n1h2txyewy System True +Microsoft.CredDialogHost cw5n1h2txyewy System True +Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned True +Microsoft.ECApp 8wekyb3d8bbwe System True +Microsoft.GamingApp 8wekyb3d8bbwe Provisioned False +Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False +Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False +Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.HEVCVideoExtension 8wekyb3d8bbwe Provisioned False +Microsoft.LockApp cw5n1h2txyewy System True +Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True +Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False +Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False +Microsoft.Paint 8wekyb3d8bbwe Provisioned False +Microsoft.People 8wekyb3d8bbwe Provisioned False +Microsoft.PowerAutomateDesktop 8wekyb3d8bbwe Provisioned False +Microsoft.RawImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False +Microsoft.SecHealthUI 8wekyb3d8bbwe Provisioned True +Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False +Microsoft.Todos 8wekyb3d8bbwe Provisioned False +Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False +Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False +Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False +Microsoft.Win32WebViewHost cw5n1h2txyewy System True +Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True +Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True +Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True +Microsoft.Windows.CapturePicker cw5n1h2txyewy System True +Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True +Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True +Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True +Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True +Microsoft.Windows.ParentalControls cw5n1h2txyewy System True +Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False +Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True +Microsoft.Windows.PrintQueueActionCenter cw5n1h2txyewy System True +Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True +Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True +Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True +Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False +microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsNotepad 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False +Microsoft.WindowsTerminal 8wekyb3d8bbwe Provisioned False +Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGameCallableUI cw5n1h2txyewy System True +Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False +Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False +Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False +Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False +MicrosoftCorporationII.QuickAssist 8wekyb3d8bbwe Provisioned False +MicrosoftWindows.Client.CBS cw5n1h2txyewy System True +MicrosoftWindows.Client.Core cw5n1h2txyewy System True +MicrosoftWindows.Client.FileExp cw5n1h2txyewy System True +MicrosoftWindows.Client.WebExperience cw5n1h2txyewy Provisioned False +MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True +NcsiUwpApp 8wekyb3d8bbwe System True +Windows.CBSPreview cw5n1h2txyewy System True +windows.immersivecontrolpanel cw5n1h2txyewy System True +Windows.PrintDialog cw5n1h2txyewy System True + + diff --git a/docs/research/windows/README.md b/docs/research/windows/README.md new file mode 100644 index 00000000..6c37d770 --- /dev/null +++ b/docs/research/windows/README.md @@ -0,0 +1,46 @@ +# Research on Windows + +In this section, we maintain a structured approach to our research on Windows. +The use of `01` prefixed file names aids in organizing and retrieving search results effectively. + +## Apps + +The PowerShell script below serves as a method for gathering detailed information about Windows packages. + +```ps1 +$allPackages = @() +$provisionedPackages = Get-AppxProvisionedPackage -Online +foreach ($installedPackage in Get-AppxPackage -AllUsers) { +if ($installedPackage.IsFramework -eq $true) { + continue +} +$allPackages += [PSCustomObject]@{ + Name = $installedPackage.Name + PublisherId = $installedPackage.PublisherId + Category = if ($installedPackage.SignatureKind -eq "System") { + 'System' + } elseif ($provisionedPackages | Where-Object { $_.DisplayName -eq $installedPackage.Name }) { + 'Provisioned' + } else { + 'Installed' + } + NonRemovable = $installedPackage.NonRemovable +} +} +foreach ($provisionedPackage in $provisionedPackages) { + if ($allPackages | Where-Object { $_.Name -eq $provisionedPackage.DisplayName }) { + continue + } + $allPackages += [PSCustomObject]@{ + Name = $provisionedPackage.DisplayName + PublisherId = $provisionedPackage.PackageName -split '_' | Select-Object -Last 1 + Category = 'Provisioned' + NonRemovable = $false + } +} +$allPackages ` + | Sort-Object Name ` + | Select-Object Name, PublisherId, Category, NonRemovable ` + | Format-Table ` + | Out-File -FilePath "$([System.Environment]::GetFolderPath('Desktop'))\apps.txt" +``` diff --git a/docs/script-guidelines.md b/docs/script-guidelines.md new file mode 100644 index 00000000..ebdf488a --- /dev/null +++ b/docs/script-guidelines.md @@ -0,0 +1,58 @@ +# privacy.sexy Script Guidelines + +Create a script for privacy.sexy by submitting a PR or creating an issue (details in [Extend Scripts](./../CONTRIBUTING.md#extend-scripts)). +As scripts are central to privacy.sexy and reach a global audience, their design is critical. + +Key attributes of a good script: + +- ✅ Well-referenced [documentation](#documentation). +- ✅ Utilizes [shared functions](#shared-functions). +- ✅ Has a [simple name](#name). + +## Name + +- Choose a title that is easy to understand for all users, regardless of technical skill, yet remains technically accurate. +- Focus on privacy implications, avoiding complex or overly technical jargon. +- Maintain consistency in naming, avoiding linguistic variations. +- Use action-oriented language for clarity and directness. Use an instruction format like "do this, do that" for clear, direct guidance. +- Respect the official casing of brand names. +- Choose clear and uncomplicated language. +- It should start with an imperative noun. +- Start with action verbs like `Clear`, `Disable`, `Remove`, `Configure`, `Minimize`, `Maximize`. While exceptions exist, these prefixes help maintain naming consistency. +- The scripts that modify hosts file should start with `Block ..`. +- Favor the terms: + - `Disable` over `Turn off`, `Stop`, `Prevent` + - `Configure` over `Set up` + - `Clear` over `Erase`, `Clean` + - `Minimize` over `Limit`, `Reduce` + - `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong` + - `Remove` over `Uninstall` + - `Improve` over `Increase` +- Structure your phrases for clarity, examples: + - Prefer `Disable XX telemetry` over `Disable telemetry in XX` + - Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`. +- Use sentence case rather than Title Case. + +## Documentation + +- Use credible and reputable sources for references. +- Use archived links by using [archive.org](https://archive.org) or [archive.ph](https://archive.ph). + - Format archive.today links fully, for example: `https://archive.ph/YYYYMMDDhhmmss/https://privacy.sexy`. +- Explain the default behavior if the script is not executed. + +## Shared functions + +Use existing shared functions when possible, like `DisableService` for disabling services,. + +- 📖 Learn about templates in [templating.md](./templating.md). +- 📖 For syntax, see [collection-files.md](collection-files.md). + +## Code + +- Prefer [shared functions](#shared-functions); avoid custom code unless necessary. +- Keep code simple and compatible with older systems. +- Focus on reliability, ensuring the script is error-resistant, works on different locales and handles unexpected situations. +- Language selection: + - Windows: Use batch when simpler, otherwise PowerShell. + - macOS/Linux: Use bash when simpler, otherwise Python. +- Provide revert code to restore original/default settings when applicable. diff --git a/docs/templating.md b/docs/templating.md new file mode 100644 index 00000000..42cc047a --- /dev/null +++ b/docs/templating.md @@ -0,0 +1,199 @@ +# Templating + +## Benefits of templating + +- **Code sharing:** Share code across scripts for consistent practices and easier maintenance. +- **Script independence:** Generate self-contained scripts, eliminating the need for external code. +- **Cleaner code:** Use pipes for complex operations, resulting in more readable and streamlined code. + +## Expressions + +**Syntax:** + +Expressions are enclosed within `{{` and `}}`. +Example: `Hello {{ $name }}!`. +They are a core component of templating, enhancing scripts with dynamic capabilities and functionality. + +**Syntax similarity:** + +The syntax shares similarities with [Go Templates ❤️](https://pkg.go.dev/text/template), but with some differences: + +**Function definitions:** + +You can use expressions in function definition. +Refer to [Function](./collection-files.md#function) for more details. + +Example usage: + +```yaml + name: GreetFunction + parameters: + - name: name + code: Hello {{ $name }}! +``` + +If you assign `name` the value `world`, invoking `GreetFunction` would result in `Hello world!`. + +**Function arguments:** + +You can also use expressions in arguments in nested function calls. +Refer to [`Function | collection-files.md`](./collection-files.md#functioncall) for more details. + +Example with nested function calls: + +```yaml +- + name: PrintMessageFunction + parameters: + - name: message + code: echo "{{ $message }}" +- + name: GreetUserFunction + parameters: + - name: userName + call: + name: PrintMessageFunction + parameters: + argument: 'Hello, {{ $userName }}!' +``` + +Here, if `userName` is `Alice`, invoking `GreetUserFunction` would execute `echo "Hello, Alice!"`. + +**Nested templates:** + +You can nest expressions inside expressions (also called "nested templates"). +This means that an expression can output another expression where compiler will compile both. + +For example, following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output: + +```go + {{ with $condition }} + echo {{ $text }} + {{ end }} +``` + +### Parameter substitution + +Parameter substitution dynamically replaces variable references with their corresponding values in the script. + +**Example function:** + +```yaml + name: DisplayTextFunction + parameters: + - name: 'text' + code: echo {{ $text }} +``` + +Invoking `DisplayTextFunction` with `text` set to `"Hello, world!"` would result in `echo "Hello, World!"`. + +### with + +The `with` expression enables conditional rendering and provides a context variable for simpler code. + +**Optional block rendering:** + +If the provided variable is falsy (`false`, `null`, or empty), the compiler skips the enclosed block of code. +A "block" lies between the with start (`{{ with .. }}`) and end (`{{ end }}`) expressions, defining its boundaries. + +Example: + +```go +{{ with $optionalVariable }} + Hello +{{ end }} +``` + +This would display `Hello` if `$optionalVariable` is truthy. + +**Parameter declaration:** + +You should set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`. + +Declare parameters used for `with` condition as optional such as: + +```yaml +name: ConditionalOutputFunction +parameters: + - name: 'data' + optional: true +code: |- + {{ with $data }} + Data is: {{ . }} + {{ end }} +``` + +**Context variable:** + +`with` statement binds its context (value of the provided parameter value) as arbitrary `.` value. +`{{ . }}` syntax gives you access to the context variable. +This is optional to use, and not required to use `with` expressions. + +For example: + +```go + {{ with $parameterName }}Parameter value is {{ . }} here {{ end }} +``` + +**Multiline text:** + +It supports multiline text inside the block. You can write something like: + +```go + {{ with $argument }} + First line + Second line + {{ end }} +``` + +**Inner expressions:** + +You can also embed other expressions inside its block, such as [parameter substitution](#parameter-substitution): + +```go + {{ with $condition }} + This is a different parameter: {{ $text }} + {{ end }} +``` + +This also includes nesting `with` statements: + +```go + {{ with $condition1 }} + Value of $condition1: {{ . }} + {{ with $condition2 }} + Value of $condition2: {{ . }} + {{ end }} + {{ end }} +``` + +### Pipes + +Pipes are functions designed for text manipulation. +They allow for a sequential application of operations resembling [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), also known as "chaining". +Each pipeline's output becomes the input of the following pipe. + +**Pre-defined**: + +Pipes are pre-defined by the system. +You cannot create pipes in [collection files](./collection-files.md). +[A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files. + +**Compatibility:** + +You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax. + +For example: + +```go +{{ with $script }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }} +``` + +**Naming:** + +❗ Pipe names must be camelCase without any space or special characters. + +**Available pipes:** + +- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line. +- `escapeDoubleQuotes`: Escapes `"` characters for batch command execution, allows you to use them inside double quotes (`"`). diff --git a/docs/tests.md b/docs/tests.md new file mode 100644 index 00000000..5b67747f --- /dev/null +++ b/docs/tests.md @@ -0,0 +1,92 @@ +# Tests + +There are different types of tests executed: + +1. [Unit tests](#unit-tests) +2. [Integration tests](#integration-tests) +3. [End-to-end (E2E) tests](#e2e-tests) +4. [Automated checks](#automated-checks) + +## Unit and integration tests + +- They utilize [Vitest](https://vitest.dev/). +- Test files are suffixed with `.spec.ts`. + +### Act, arrange, assert + +- Tests implement the act, arrange, and assert (AAA) pattern. +- **Arrange** + - Sets up the test scenario and environment. + - Begins with comment line `// arrange`. +- **Act** + - Executes the actual test. + - Begins with comment line `// act`. +- **Assert** + - Sets an expectation for the test's outcome. + - Begins with comment line `// assert`. + +### Unit tests + +- Evaluate individual components in isolation. +- Located in [`./tests/unit`](./../tests/unit). +- Achieve isolation using stubs where you place: + - Common stubs in [`./shared/Stubs`](./../tests/unit/shared/Stubs), + - Component-specific stubs in same folder as test file. +- Include Vue component tests, enabled by `@vue/test-utils`. + +#### Unit tests naming + +- Test suites start with a description of the component or system under test. + - E.g., tests for `Application.ts` are contained in `Application.spec.ts`. +- Whenever possible, `describe` blocks group tests of the same function. + - E.g., tests for `run()` are inside `describe('run', () => ...)`. + +### Integration tests + +- Assess the combined functionality of components. +- They verify that third-party dependencies function as anticipated. + +## E2E tests + +- Examine the live web application's functionality and performance. +- Uses Cypress to run the tests. + +## Automated checks + +These checks validate various qualities like runtime execution, building process, security testing, etc. + +- Use [various tools](./../package.json) and [scripts](./../scripts). +- Are automatically executed as [GitHub workflows](./../.github/workflows). + +### Security checks + +- [`checks.security.sast`](./../.github/workflows/checks.security.sast.yaml): Utilizes CodeQL to conduct Static Analysis Security Testing (SAST) to ensure the secure integrity of the codebase. +- [`checks.security.dependencies`](./../.github/workflows/checks.security.dependencies.yaml): Performs audits on third-party dependencies to identify and mitigate potential vulnerabilities, safeguarding the project from exploitable weaknesses. + +## Tests structure + +- [`package.json`](./../package.json): Defines test commands and includes tools used in tests. +- [`vite.config.ts`](./../vite.config.ts): Configures `vitest` for unit and integration tests. +- [`./src/`](./../src/): Contains the code subject to testing. +- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories. + - [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests. + - [`Assertions/`](./../tests/shared/Assertions/): Contains common assertion functions, prefixed with `expect`. +- [`./tests/unit/`](./../tests/unit/) + - Stores unit test code. + - The directory structure mirrors [`./src/`](./../src). + - E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts). + - [`shared/`](./../tests/unit/shared/) + - Contains shared unit test functionalities. + - [`TestCases/`](./../tests/unit/shared/TestCases/) + - Shared test cases. + - Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix. + - [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities. +- [`./tests/integration/`](./../tests/integration/): Contains integration test files. +- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file. +- [`cypress-dirs.json`](./../cypress-dirs.json): A central definition of directories used by Cypress, designed for reuse across different configurations. +- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension. + - [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation. + - *(git ignored)* `/videos`: Asset folder for videos taken during tests. + - *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests. + - [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single test file. + - [`/support/interactions/`](./../tests/e2e/support/interactions/): Contains reusable functions for simulating user interactions, enhancing test readability and maintainability. diff --git a/electron-builder.cjs b/electron-builder.cjs new file mode 100644 index 00000000..383f1a78 --- /dev/null +++ b/electron-builder.cjs @@ -0,0 +1,74 @@ +/* eslint-disable no-template-curly-in-string */ + +const { join, resolve } = require('node:path'); +const { readdirSync, existsSync } = require('node:fs'); +const { electronBundled, electronUnbundled } = require('./dist-dirs.json'); + +/** +* @type {import('electron-builder').Configuration} +* @see https://www.electron.build/configuration/configuration +*/ +module.exports = { + // Common options + publish: { + provider: 'github', + vPrefixedTagName: false, // default: true + releaseType: 'release', // default: draft + }, + directories: { + output: electronBundled, + buildResources: resolvePathFromProjectRoot('src/presentation/electron/build'), + }, + extraMetadata: { + main: findMainEntryFile( + join(electronUnbundled, 'main'), // do not `path.resolve`, it expects a relative path + ), + }, + + // Windows + win: { + target: 'nsis', + }, + nsis: { + artifactName: '${name}-Setup-${version}.${ext}', + }, + + // Linux + linux: { + target: 'AppImage', + }, + appImage: { + artifactName: '${name}-${version}.${ext}', + }, + + // macOS + mac: { + target: { + target: 'dmg', + arch: 'universal', + }, + }, + dmg: { + artifactName: '${name}-${version}.${ext}', + }, +}; + +/** + * Finds by accommodating different JS file extensions and module formats. + */ +function findMainEntryFile(parentDirectory) { + const absoluteParentDirectory = resolvePathFromProjectRoot(parentDirectory); + if (!existsSync(absoluteParentDirectory)) { + return null; // Avoid disrupting other processes such `npm install`. + } + const files = readdirSync(absoluteParentDirectory); + const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file)); + if (!entryFile) { + throw new Error(`Main entry file not found in ${absoluteParentDirectory}.`); + } + return join(parentDirectory, entryFile); +} + +function resolvePathFromProjectRoot(pathSegment) { + return resolve(__dirname, pathSegment); +} diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 00000000..c778e502 --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,92 @@ +import { resolve } from 'node:path'; +import { mergeConfig, type UserConfig } from 'vite'; +import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; +import { getAliases, getClientEnvironmentVariables } from './vite-config-helper'; +import { createVueConfig } from './vite.config'; +import distDirs from './dist-dirs.json' assert { type: 'json' }; + +const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts'); +const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts'); +const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html'); +const ELECTRON_DIST_SUBDIRECTORIES = { + main: resolveElectronDistSubdirectory('main'), + preload: resolveElectronDistSubdirectory('preload'), + renderer: resolveElectronDistSubdirectory('renderer'), +}; + +process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.mjs'); + +export default defineConfig({ + main: getSharedElectronConfig({ + distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.main, + entryFilePath: MAIN_ENTRY_FILE, + }), + preload: getSharedElectronConfig({ + distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.preload, + entryFilePath: PRELOAD_ENTRY_FILE, + }), + renderer: mergeConfig( + createVueConfig({ + supportLegacyBrowsers: false, + }), + { + build: { + outDir: ELECTRON_DIST_SUBDIRECTORIES.renderer, + rollupOptions: { + input: { + index: WEB_INDEX_HTML_PATH, + }, + }, + }, + }, + ), +}); + +function getSharedElectronConfig(options: { + readonly distDirSubfolder: string; + readonly entryFilePath: string; +}): UserConfig { + return { + build: { + outDir: options.distDirSubfolder, + lib: { + entry: options.entryFilePath, + }, + rollupOptions: { + output: { + format: 'es', + + // Ensure all generated files use '.mjs' for module consistency. + // Otherwise, preloader process get `.mjs` extension but main process get `.js` extension, see https://github.com/alex8088/electron-vite/issues/397. + entryFileNames: '[name].mjs', + }, + }, + }, + plugins: [externalizeDepsPlugin({ + exclude: [ + // Keep 'electron-log' in bundling process. + // This is a workaround for inability of Electron's ESM loader to resolve subpath imports. + // Do not externalize `electron-log` so subpath imports such as `electron-log/main` works. + // See https://github.com/electron/electron/issues/41241, https://github.com/alex8088/electron-vite/issues/401 + 'electron-log', + ], + })], + define: { + ...getClientEnvironmentVariables(), + }, + resolve: { + alias: { + ...getAliases(), + }, + }, + }; +} + +function resolvePathFromProjectRoot(pathSegment: string): string { + return resolve(__dirname, pathSegment); +} + +function resolveElectronDistSubdirectory(subDirectory: string): string { + const electronDistDir = resolvePathFromProjectRoot(distDirs.electronUnbundled); + return resolve(electronDistDir, subDirectory); +} diff --git a/img/README.md b/img/README.md new file mode 100644 index 00000000..34ed639c --- /dev/null +++ b/img/README.md @@ -0,0 +1,9 @@ +# img + +This folder contains image files and other resources related to images. + +## logo.svg + +[`logo.svg`](./logo.svg) serves as the primary logo from which all other icons and images are derived. +Only modify this file manually. +After making changes, execute `npm run build:icons` to regenerate logo files in various formats. diff --git a/img/architecture/app-ddd.drawio.png b/img/architecture/app-ddd.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..e53e0db108ae34ac7f9ecc5f4ec8b1b6406d4220 GIT binary patch literal 64572 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

&*&P6V)za$$o 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 literal 0 HcmV?d00001 diff --git a/img/architecture/app-state.drawio b/img/architecture/app-state.drawio new file mode 100644 index 00000000..0d27680b --- /dev/null +++ b/img/architecture/app-state.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/img/architecture/app-state.png b/img/architecture/app-state.png new file mode 100644 index 0000000000000000000000000000000000000000..524179385f5e6bf68f1e5661831fcde1d8e0f43e GIT binary patch literal 25586 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 \ No newline at end of file diff --git a/img/architecture/aws-solution.png b/img/architecture/aws-solution.png new file mode 100644 index 0000000000000000000000000000000000000000..e8c86ad9fb3051781bbffb6ff317bb655385e71c GIT binary patch literal 58590 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

w^!a874m-;OS#oO?x5!=Jzr& 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 \ No newline at end of file diff --git a/img/architecture/gitops.png b/img/architecture/gitops.png new file mode 100644 index 0000000000000000000000000000000000000000..238efd8c18f3c4655cde94eca1097450db006acb GIT binary patch literal 261511 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 AwEzGB literal 0 HcmV?d00001 diff --git a/img/logo.svg b/img/logo.svg new file mode 100644 index 00000000..08db5577 --- /dev/null +++ b/img/logo.svg @@ -0,0 +1,56 @@ + + + + + + diff --git a/img/screenshot.png b/img/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..ba296630930f751306225d64f7e8a26995001dc3 GIT binary patch literal 118357 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%0DQ$=FtAG_Ft!CWC^>uc@3)^c5$xX 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 u|C-HR>Y=6.0.0'} + + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.25.4': + resolution: {integrity: sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.25.2': + resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.25.5': + resolution: {integrity: sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.24.7': + resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': + resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.2': + resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.25.4': + resolution: {integrity: sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.25.2': + resolution: {integrity: sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.2': + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-member-expression-to-functions@7.24.8': + resolution: {integrity: sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.24.7': + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.25.2': + resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.24.7': + resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.24.8': + resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.25.0': + resolution: {integrity: sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.25.0': + resolution: {integrity: sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-simple-access@7.24.7': + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-skip-transparent-expression-wrappers@7.24.7': + resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.24.8': + resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.25.0': + resolution: {integrity: sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.25.0': + resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.4': + resolution: {integrity: sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3': + resolution: {integrity: sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.0': + resolution: {integrity: sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.0': + resolution: {integrity: sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7': + resolution: {integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.0': + resolution: {integrity: sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-export-namespace-from@7.8.3': + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.24.7': + resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.24.7': + resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.24.7': + resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.25.4': + resolution: {integrity: sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.24.7': + resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.24.7': + resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.25.0': + resolution: {integrity: sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.25.4': + resolution: {integrity: sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.24.7': + resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.25.4': + resolution: {integrity: sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.24.7': + resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.24.8': + resolution: {integrity: sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.24.7': + resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.24.7': + resolution: {integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.0': + resolution: {integrity: sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.24.7': + resolution: {integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.24.7': + resolution: {integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.24.7': + resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.24.7': + resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.25.1': + resolution: {integrity: sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.24.7': + resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.25.2': + resolution: {integrity: sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.24.7': + resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.24.7': + resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.24.7': + resolution: {integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.24.8': + resolution: {integrity: sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.25.0': + resolution: {integrity: sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.24.7': + resolution: {integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.24.7': + resolution: {integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.24.7': + resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.24.7': + resolution: {integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.24.7': + resolution: {integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.24.7': + resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.24.7': + resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.24.7': + resolution: {integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.24.8': + resolution: {integrity: sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.24.7': + resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.25.4': + resolution: {integrity: sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.24.7': + resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.24.7': + resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.24.7': + resolution: {integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-reserved-words@7.24.7': + resolution: {integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.24.7': + resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.24.7': + resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.24.7': + resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.24.7': + resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.24.8': + resolution: {integrity: sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.24.7': + resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.24.7': + resolution: {integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.24.7': + resolution: {integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.25.4': + resolution: {integrity: sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.25.4': + resolution: {integrity: sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/regjsgen@0.8.0': + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + + '@babel/runtime@7.25.4': + resolution: {integrity: sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.0': + resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.4': + resolution: {integrity: sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.25.4': + resolution: {integrity: sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==} + engines: {node: '>=6.9.0'} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cypress/request@3.0.1': + resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==} + engines: {node: '>= 6'} + + '@cypress/xvfb@1.2.4': + resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} + + '@develar/schema-utils@2.6.5': + resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} + engines: {node: '>= 8.9.0'} + + '@electron/asar@3.2.10': + resolution: {integrity: sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + + '@electron/notarize@2.2.1': + resolution: {integrity: sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.0.5': + resolution: {integrity: sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/universal@1.5.1': + resolution: {integrity: sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==} + engines: {node: '>=8.6'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.0': + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint-types/import@2.29.0-1': + resolution: {integrity: sha512-cmlKmWLY9PKmsxKdb5DlXZBe6SV3fIyhoMj+lBVUeW5d9Dvqs62Hk+jskz4D9B3x+dEwDez29ScTog04K8WpUQ==} + + '@eslint-types/typescript-eslint@6.21.0': + resolution: {integrity: sha512-ao4TdMLw+zFdAJ9q6iBBxC5GSrJ14Hpv0VKaergr++jRTDaGgoYiAq84tx1FYqUJzQgzJC7dm6s52IAQP7EiHA==} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.0': + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@floating-ui/core@1.6.7': + resolution: {integrity: sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==} + + '@floating-ui/dom@1.6.10': + resolution: {integrity: sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==} + + '@floating-ui/utils@0.2.7': + resolution: {integrity: sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==} + + '@floating-ui/vue@1.1.4': + resolution: {integrity: sha512-ammH7T3vyCx7pmm9OF19Wc42zrGnUw0QvLoidgypWsCLJMtGXEwY7paYIHO+K+oLC3mbWpzIHzeTVienYenlNg==} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + + '@malept/cross-spawn-promise@1.1.1': + resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} + engines: {node: '>= 10'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@modyfi/vite-plugin-yaml@1.1.0': + resolution: {integrity: sha512-L26xfzkSo1yamODCAtk/ipVlL6OEw2bcJ92zunyHu8zxi7+meV0zefA9xscRMDCsMY8xL3C3wi3DhMiPxcbxbw==} + peerDependencies: + vite: ^3.2.7 || ^4.0.5 || ^5.0.5 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/config@8.3.4': + resolution: {integrity: sha512-01rtHedemDNhUXdicU7s+QYz/3JyV5Naj84cvdXGH4mgCdL+agmSYaLF4LUG4vMCLzhBO8YtS0gPpH1FGvbgAw==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@npmcli/git@5.0.8': + resolution: {integrity: sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@npmcli/map-workspaces@3.0.6': + resolution: {integrity: sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@npmcli/name-from-folder@2.0.0': + resolution: {integrity: sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@npmcli/package-json@5.2.0': + resolution: {integrity: sha512-qe/kiqqkW0AGtvBjL8TJKZk/eBBSpnJkUWvHdQ9jM2lKHXRYYJuyNpJPlJw3c8QjC2ow6NZYiLExhUaeJelbxQ==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@npmcli/promise-spawn@7.0.2': + resolution: {integrity: sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/pluginutils@5.1.0': + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.21.0': + resolution: {integrity: sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.21.0': + resolution: {integrity: sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.21.0': + resolution: {integrity: sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.21.0': + resolution: {integrity: sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm-gnueabihf@4.21.0': + resolution: {integrity: sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.21.0': + resolution: {integrity: sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.21.0': + resolution: {integrity: sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.21.0': + resolution: {integrity: sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.21.0': + resolution: {integrity: sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.21.0': + resolution: {integrity: sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.21.0': + resolution: {integrity: sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.21.0': + resolution: {integrity: sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.21.0': + resolution: {integrity: sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.21.0': + resolution: {integrity: sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.21.0': + resolution: {integrity: sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.21.0': + resolution: {integrity: sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==} + cpu: [x64] + os: [win32] + + '@rushstack/eslint-patch@1.10.4': + resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@sindresorhus/is@0.14.0': + resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} + engines: {node: '>=6'} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@szmarczak/http-timer@1.1.2': + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@types/ace@0.0.52': + resolution: {integrity: sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/concat-stream@2.0.3': + resolution: {integrity: sha512-3qe4oQAPNwVNwK4C9c8u+VJqv9kez+2MR4qJpoPFfXtgxxif1QbFusvXzK0/Wra2VX07smostI2VMmJNSpZjuQ==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/hosted-git-info@3.0.5': + resolution: {integrity: sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/is-empty@1.2.3': + resolution: {integrity: sha512-4J1l5d79hoIvsrKh5VUKVRA1aIdsOb10Hu5j3J2VfP/msDnfTdGPmNp2E1Wg+vs97Bktzo+MZePFFXSGoykYJw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/node@20.16.1': + resolution: {integrity: sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==} + + '@types/node@22.5.0': + resolution: {integrity: sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==} + + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + + '@types/sinonjs__fake-timers@8.1.1': + resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} + + '@types/sizzle@2.3.8': + resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==} + + '@types/supports-color@8.1.3': + resolution: {integrity: sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==} + + '@types/text-table@0.2.5': + resolution: {integrity: sha512-hcZhlNvMkQG/k1vcZ6yHOl6WAYftQ2MLfTHcYRZ2xYZFD8tGVnE3qFV0lj1smQeDSR7/yY0PyuUalauf33bJeA==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/verror@1.10.10': + resolution: {integrity: sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@typescript-eslint/eslint-plugin@6.21.0': + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@6.21.0': + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@6.21.0': + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/type-utils@6.21.0': + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@6.21.0': + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/typescript-estree@6.21.0': + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@6.21.0': + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@6.21.0': + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@vitejs/plugin-legacy@5.4.2': + resolution: {integrity: sha512-hlyyQL+wEIyOWdwsUKX+0g3kBU4AbHmVzHarLvVKiGGGqLIYjttMvvjk6zGY8RD9dab6QuFNhDoxg0YFhQ26xA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + terser: ^5.4.0 + vite: ^5.0.0 + + '@vitejs/plugin-vue@5.1.2': + resolution: {integrity: sha512-nY9IwH12qeiJqumTCLJLE7IiNx7HZ39cbHaysEUd+Myvbz9KAqd2yq+U01Kab1R/H1BmiyM2ShTYlNH32Fzo3A==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 + vue: ^3.2.25 + + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/runner@2.0.5': + resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} + + '@vitest/snapshot@2.0.5': + resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} + + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + + '@volar/language-core@2.4.0': + resolution: {integrity: sha512-FTla+khE+sYK0qJP+6hwPAAUwiNHVMph4RUXpxf/FIPKUP61NFrVZorml4mjFShnueR2y9/j8/vnh09YwVdH7A==} + + '@volar/source-map@2.4.0': + resolution: {integrity: sha512-2ceY8/NEZvN6F44TXw2qRP6AQsvCYhV2bxaBPWxV9HqIfkbRydSksTFObCF1DBDNBfKiZTS8G/4vqV6cvjdOIQ==} + + '@volar/typescript@2.4.0': + resolution: {integrity: sha512-9zx3lQWgHmVd+JRRAHUSRiEhe4TlzL7U7e6ulWXOxHH/WNYxzKwCvZD7WYWEZFdw4dHfTD9vUR0yPQO6GilCaQ==} + + '@vue/compiler-core@3.4.38': + resolution: {integrity: sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==} + + '@vue/compiler-dom@3.4.38': + resolution: {integrity: sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==} + + '@vue/compiler-sfc@3.4.38': + resolution: {integrity: sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==} + + '@vue/compiler-ssr@3.4.38': + resolution: {integrity: sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/eslint-config-airbnb-with-typescript@8.0.0': + resolution: {integrity: sha512-gMaApQBRAOebJtI9NyDfic7XhjXGramJXx/PHcOQBUkmpPhDCDeX7N0oQqZKR6cY9la7LDvELfPRs62KJcAtjg==} + peerDependencies: + eslint: ^8.2.0 + eslint-plugin-vue: ^9.2.0 + typescript: '*' + + '@vue/eslint-config-airbnb@8.0.0': + resolution: {integrity: sha512-0PGJubVK8+arJC+07xeL7gFLLfr5hxub7UCl+x+bxgvE2qtJodbOXZ27mdt1tAYsgUuhmp3ymn9mNbAIvNGahA==} + peerDependencies: + eslint: ^8.2.0 + eslint-plugin-vue: ^9.2.0 + + '@vue/eslint-config-typescript@12.0.0': + resolution: {integrity: sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 + eslint-plugin-vue: ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@2.0.29': + resolution: {integrity: sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.4.38': + resolution: {integrity: sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==} + + '@vue/runtime-core@3.4.38': + resolution: {integrity: sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==} + + '@vue/runtime-dom@3.4.38': + resolution: {integrity: sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==} + + '@vue/server-renderer@3.4.38': + resolution: {integrity: sha512-NggOTr82FbPEkkUvBm4fTGcwUY8UuTsnWC/L2YZBmvaQ4C4Jl/Ao4HHTB+l7WnFCt5M/dN3l0XLuyjzswGYVCA==} + peerDependencies: + vue: 3.4.38 + + '@vue/shared@3.4.38': + resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==} + + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ace-builds@1.36.0: + resolution: {integrity: sha512-7to4F86V5N13EY4M9LWaGo2Wmr9iWe5CrYpc28F+/OyYCf7yd+xBV5x9v/GB73EBGGoYd89m6JjeIUjkL6Yw+w==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + app-builder-bin@4.0.0: + resolution: {integrity: sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==} + + app-builder-lib@24.13.3: + resolution: {integrity: sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 24.13.3 + electron-builder-squirrel-windows: 24.13.3 + + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.1: + resolution: {integrity: sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA==} + + axe-core@4.10.0: + resolution: {integrity: sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==} + engines: {node: '>=4'} + + axios@1.7.5: + resolution: {integrity: sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==} + + axobject-query@3.1.1: + resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} + + babel-plugin-polyfill-corejs2@0.4.11: + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.10.6: + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.2: + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + blob-util@2.0.2: + resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} + + bluebird-lst@1.0.9: + resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist-to-esbuild@2.1.1: + resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + browserslist: '*' + + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal@1.0.1: + resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==} + engines: {node: '>=0.4'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builder-util-runtime@9.2.4: + resolution: {integrity: sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==} + engines: {node: '>=12.0.0'} + + builder-util@24.13.1: + resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@6.1.0: + resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} + engines: {node: '>=8'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + cachedir@2.4.0: + resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} + engines: {node: '>=6'} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001651: + resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + check-links@1.1.8: + resolution: {integrity: sha512-lxt1EeQ1CVkmiZzPfbPufperYK0t7MvhdLs3zlRH9areA6NVT1tcGymAdJONolNWQBdCFU/sek59RpeLmVHCnw==} + engines: {node: '>=8'} + + check-more-types@2.24.0: + resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} + engines: {node: '>= 0.8.0'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + ci-info@4.0.0: + resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} + engines: {node: '>=8'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + co@3.1.0: + resolution: {integrity: sha512-CQsjCRiNObI8AtTsNIBDRMQ4oMR83CzEswHYahClvul7gKk+lDQiOKv+5qh7LQWf5sh6jkZNispz/QlsZxyNgA==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + + computeds@0.0.1: + resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + config-file-ts@0.2.6: + resolution: {integrity: sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==} + + confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + + consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js-compat@3.38.1: + resolution: {integrity: sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==} + + core-js@3.38.1: + resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@4.0.1: + resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} + engines: {node: '>=18'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + cypress@13.13.3: + resolution: {integrity: sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==} + engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} + hasBin: true + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + decompress-response@3.3.0: + resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} + engines: {node: '>=4'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defer-to-connect@1.1.3: + resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dir-compare@3.3.0: + resolution: {integrity: sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dmg-builder@24.13.3: + resolution: {integrity: sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==} + + dmg-license@1.0.11: + resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + engines: {node: '>=8'} + os: [darwin] + hasBin: true + + dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + + dns-socket@4.2.2: + resolution: {integrity: sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg==} + engines: {node: '>=6'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dotenv-expand@5.1.0: + resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + + dotenv@9.0.2: + resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} + engines: {node: '>=10'} + + duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@24.13.3: + resolution: {integrity: sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==} + + electron-builder@24.13.3: + resolution: {integrity: sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-devtools-installer@3.2.0: + resolution: {integrity: sha512-t3UczsYugm4OAbqvdImMCImIMVdFzJAHgbwHpkl5jmfu1izVgUcP/mnrPqJIpEeCK1uZGpt+yHgWEN+9EwoYhQ==} + + electron-log@5.1.7: + resolution: {integrity: sha512-/PjrS9zGkrZCDTHt6IgNE3FeciBbi4wd7U76NG9jAoNXF99E9IJdvBkqvaUJ1NjLojYDKs0kTvn9YhKy1/Zi+Q==} + engines: {node: '>= 14'} + + electron-progressbar@2.2.1: + resolution: {integrity: sha512-LQ9bxM3Tf5PG/1QngY8ywvht7IKvQ8tEIra8uh3RkLASqN/GYvr4r0uU9qz38r0Zn72UcouYzumKDfLwoI/rsw==} + + electron-publish@24.13.1: + resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==} + + electron-to-chromium@1.5.13: + resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==} + + electron-updater@6.2.1: + resolution: {integrity: sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==} + + electron-vite@2.3.0: + resolution: {integrity: sha512-lsN2FymgJlp4k6MrcsphGqZQ9fKRdJKasoaiwIrAewN1tapYI/KINLdfEL7n10LuF0pPSNf/IqjzZbB5VINctg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@swc/core': ^1.0.0 + vite: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@swc/core': + optional: true + + electron@31.4.0: + resolution: {integrity: sha512-YTwKoAA+nrJMlI1TTHnIXLYWoQLKnhbkz0qxZcI7Hadcy0UaFMFs9xzwvH2MnrRpVJy7RKo49kVGuvSdRl8zMA==} + engines: {node: '>= 12.20.55'} + hasBin: true + + emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + + es-iterator-helpers@1.0.19: + resolution: {integrity: sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-airbnb-base@15.0.0: + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + + eslint-config-airbnb-typescript@17.1.0: + resolution: {integrity: sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.13.0 || ^6.0.0 + '@typescript-eslint/parser': ^5.0.0 || ^6.0.0 + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + + eslint-define-config@2.1.0: + resolution: {integrity: sha512-QUp6pM9pjKEVannNAbSJNeRuYwW3LshejfyBBpjeMGaJjaDUpVps4C6KVR8R7dWZnD3i0synmrE36znjTkJvdQ==} + engines: {node: '>=18.0.0', npm: '>=9.0.0', pnpm: '>=8.6.0'} + + eslint-import-resolver-custom-alias@1.3.2: + resolution: {integrity: sha512-wBPcZA2k6/IXaT8FsLMyiyVSG6WVEuaYIAbeKLXeGwr523BmeB9lKAAoLJWSqp3txsnU4gpkgD2x1q6K8k0uDQ==} + peerDependencies: + eslint-plugin-import: '>=2.2.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.6.1: + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + + eslint-module-utils@2.8.1: + resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-cypress@3.5.0: + resolution: {integrity: sha512-JZQ6XnBTNI8h1B9M7wJSFzc48SYbh7VMMKaNTQOFa3BQlnmXPrVc4PKen8R+fpv6VleiPeej6VxloGb42zdRvw==} + peerDependencies: + eslint: '>=7' + + eslint-plugin-import@2.29.1: + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.9.0: + resolution: {integrity: sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + eslint-plugin-react@7.35.0: + resolution: {integrity: sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-plugin-vue@9.27.0: + resolution: {integrity: sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-vuejs-accessibility@2.4.1: + resolution: {integrity: sha512-ZRZhPdslplZXSF71MtSG+zXYRAT5KiHR4JVuo/DERQf9noAkDvi5W418VOE1qllmJd7wTenndxi1q8XeDMxdHw==} + engines: {node: '>=16.0.0'} + peerDependencies: + eslint: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-stream@3.3.4: + resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + + eventemitter2@6.4.7: + resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} + + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.7.6: + resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} + + getos@3.2.1: + resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + got@9.6.0: + resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} + engines: {node: '>=8.6'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http-signature@1.3.6: + resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} + engines: {node: '>=0.10'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + iconv-corefoundation@1.1.7: + resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + engines: {node: ^8.11.2 || >=10} + os: [darwin] + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + + ip-regex@4.3.0: + resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} + engines: {node: '>=8'} + + is-absolute-url@2.1.0: + resolution: {integrity: sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==} + engines: {node: '>=0.10.0'} + + is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + + is-empty@1.2.0: + resolution: {integrity: sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.0.2: + resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-ip@3.1.0: + resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==} + engines: {node: '>=8'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-online@8.5.1: + resolution: {integrity: sha512-RKyTQx/rJqw2QOXHwy7TmXdlkpe0Hhj7GBsr6TQJaj4ebNOfameZCMspU5vYbwBBzJ2brWArdSvNVox6T6oCTQ==} + engines: {node: '>=8'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + + is-relative-url@2.0.0: + resolution: {integrity: sha512-UMyEi3F+Rvjpc29IAQQ5OuMoKylt8npO0eQdXPQ2M3A5iFvh1qG+MtiLQR2tyHcVVsqwWrQiztjPAe9hnSHYeQ==} + engines: {node: '>=0.10.0'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + + is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + is-weakset@2.0.3: + resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.2: + resolution: {integrity: sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==} + engines: {node: '>= 18.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + iterator.prototype@1.1.2: + resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + + js-beautify@1.15.1: + resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + jsdom@24.1.1: + resolution: {integrity: sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + json-buffer@3.0.0: + resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + + jsprim@2.0.2: + resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} + engines: {'0': node >=0.6.0} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + keyv@3.1.0: + resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + lazy-ass@1.6.0: + resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} + engines: {node: '> 0.8'} + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + levenshtein-edit-distance@1.0.0: + resolution: {integrity: sha512-gpgBvPn7IFIAL32f0o6Nsh2g+5uOvkt4eK9epTfgE4YVxBxwVhJ/p1888lMm/u8mXdu1ETLSi6zeEmkBI+0F3w==} + hasBin: true + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + listr2@3.14.0: + resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} + engines: {node: '>=10.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + load-plugin@6.0.3: + resolution: {integrity: sha512-kc0X2FEUZr145odl68frm+lMJuQ23+rTXYmR6TImqPtbpmXC4vVXbWKDQ9IzndA0HfyQamWfKLhzsqGSTxE63w==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + + lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + + map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + + map-stream@0.1.0: + resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + markdownlint-cli@0.41.0: + resolution: {integrity: sha512-kp29tKrMKdn+xonfefjp3a/MsNzAd9c5ke0ydMEI9PR98bOjzglYN4nfMSaIs69msUf1DNkgevAIAPtK2SeX0Q==} + engines: {node: '>=18'} + hasBin: true + + markdownlint-micromark@0.1.9: + resolution: {integrity: sha512-5hVs/DzAFa8XqYosbEAEg6ok6MF2smDj89ztn9pKkCtdKHVdPQuGMH7frFfYL9mLkvfFe4pTyAMffLbjf3/EyA==} + engines: {node: '>=18'} + + markdownlint@0.34.0: + resolution: {integrity: sha512-qwGyuyKwjkEMOJ10XN6OTKNOVYvOIi35RNvDLNxTof5s8UmyGHlCdpngRHoRGNvQVGuxO3BJ7uNSgdeX166WXw==} + engines: {node: '>=18'} + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + mdast-comment-marker@3.0.0: + resolution: {integrity: sha512-bt08sLmTNg00/UtVDiqZKocxqvQqqyQZAg1uaRuO/4ysXV5motg7RolF5o5yy/sY1rG0v2XgZEqFWho1+2UquA==} + + mdast-util-from-markdown@2.0.1: + resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} + + mdast-util-heading-style@3.0.0: + resolution: {integrity: sha512-tsUfM9Kj9msjlemA/38Z3pvraQay880E3zP2NgIthMoGcpU9bcPX9oSM6QC/+eFXGGB4ba+VCB1dKAPHB7Veug==} + + mdast-util-mdx-expression@2.0.0: + resolution: {integrity: sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.0: + resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + mem@4.3.0: + resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==} + engines: {node: '>=6'} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.1: + resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==} + + micromark-factory-destination@2.0.0: + resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} + + micromark-factory-label@2.0.0: + resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} + + micromark-factory-space@2.0.0: + resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} + + micromark-factory-title@2.0.0: + resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} + + micromark-factory-whitespace@2.0.0: + resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} + + micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + + micromark-util-chunked@2.0.0: + resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} + + micromark-util-classify-character@2.0.0: + resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} + + micromark-util-combine-extensions@2.0.0: + resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} + + micromark-util-decode-numeric-character-reference@2.0.1: + resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} + + micromark-util-decode-string@2.0.0: + resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} + + micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + micromark-util-html-tag-name@2.0.0: + resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} + + micromark-util-normalize-identifier@2.0.0: + resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} + + micromark-util-resolve-all@2.0.0: + resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} + + micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + + micromark-util-subtokenize@2.0.1: + resolution: {integrity: sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==} + + micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + micromark@4.0.0: + resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + nconf@0.12.1: + resolution: {integrity: sha512-p2cfF+B3XXacQdswUYWZ0w6Vld0832A/tuqjLBu3H1sfUcby4N2oVbGhyuCkZv+t3iY3aiFEj7gZGqax9Q2c1w==} + engines: {node: '>= 0.4.0'} + + node-addon-api@1.7.2: + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-package-data@6.0.2: + resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} + engines: {node: ^16.14.0 || >=18.0.0} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + npm-install-checks@6.3.0: + resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-normalize-package-bin@3.0.1: + resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-package-arg@11.0.3: + resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} + engines: {node: ^16.14.0 || >=18.0.0} + + npm-pick-manifest@9.1.0: + resolution: {integrity: sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==} + engines: {node: ^16.14.0 || >=18.0.0} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nwsapi@2.2.12: + resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.entries@1.1.8: + resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ospath@1.2.2: + resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + + p-any@2.1.0: + resolution: {integrity: sha512-JAERcaMBLYKMq+voYw36+x5Dgh47+/o7yuv2oQYuSSUml4YeqJEFznBrY2UeEkoSHqBua6hz518n/PsowTYLLg==} + engines: {node: '>=8'} + + p-cancelable@1.1.0: + resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} + engines: {node: '>=6'} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-is-promise@2.1.0: + resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-memoize@2.1.0: + resolution: {integrity: sha512-c6+a2iV4JyX0r4+i2IBJYO0r6LZAT2fg/tcB6GQbv1uzZsfsmKT7Ej5DRT1G6Wi7XUJSV2ZiP9+YEtluvhCmkg==} + engines: {node: '>=6'} + + p-some@4.1.0: + resolution: {integrity: sha512-MF/HIbq6GeBqTrTIl5OJubzkGU+qfFhAFi0gnTAK6rgEIJIknEiABHOTtQu4e6JiXjIwuMPMUFQzyHh5QjCl1g==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@7.1.1: + resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} + engines: {node: '>=16'} + + parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + + pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.41: + resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prepend-http@2.0.0: + resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} + engines: {node: '>=4'} + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + proc-log@4.2.0: + resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + propose@0.0.5: + resolution: {integrity: sha512-Jary1vb+ap2DIwOGfyiadcK4x1Iu3pzpkDBy8tljFPmQvnc9ES3m1PMZOMiWOG50cfoAyYNtGeBzrp+Rlh4G9A==} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + proxy-from-env@1.0.0: + resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + ps-tree@1.2.0: + resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} + engines: {node: '>= 0.10'} + hasBin: true + + psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + + public-ip@4.0.4: + resolution: {integrity: sha512-EJ0VMV2vF6Cu7BIPo3IMW1Maq6ME+fbR0NcPmqDfpfNGIRPue1X8QrGjrg/rfjDkOsIkKHIf2S5FlEa48hFMTA==} + engines: {node: '>=8'} + + pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.10.4: + resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} + engines: {node: '>=0.6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + read-config-file@6.3.2: + resolution: {integrity: sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==} + engines: {node: '>=12.0.0'} + + read-package-json-fast@3.0.2: + resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + reflect.getprototypeof@1.0.6: + resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.1.1: + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + + regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + + regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + + regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + + remark-cli@12.0.1: + resolution: {integrity: sha512-2NAEOACoTgo+e+YAaCTODqbrWyhMVmlUyjxNCkTrDRHHQvH6+NbrnqVvQaLH/Q8Ket3v90A43dgAJmXv8y5Tkw==} + hasBin: true + + remark-lint-blockquote-indentation@4.0.0: + resolution: {integrity: sha512-hdUvn+KsJbBKpY9jLY01PmfpJ/WGhLu9GJMXQGU8ADXJc+F5DWSgKAr6GQ1IUKqvGYdEML/KZ61WomWFUuecVA==} + + remark-lint-checkbox-character-style@5.0.0: + resolution: {integrity: sha512-K0G/Nok59fb2q5KUxcemBVt+ymnhTkDVLJAatZ4PAh9At8y0DGctHdU27jWsuvO0Fs7Zy62Usk7IJE2VO89p1w==} + + remark-lint-code-block-style@4.0.0: + resolution: {integrity: sha512-LKBKMVruEO0tzDnnnqi1TfUcnwY6Mo7cVtZM4E4pKt3KMhtvgU2wD68/MxDOEJd0pmnLrEgIadv74bY0gWhZpg==} + + remark-lint-emphasis-marker@4.0.0: + resolution: {integrity: sha512-xIRiB4PFWUOyIslN/UOPL6Lh+J0VD4R11+jo+W4hpGMNsg58l+2SgtdbinlXzDeoBxmaaka9n/sYpJ7cJWEIPQ==} + + remark-lint-fenced-code-marker@4.0.0: + resolution: {integrity: sha512-WFN88Rx78m4/HSbW3Kx2XAYbVfzYns4bJd9qpwDD90DA3nc59zciYd01xi6Bk3n9vSs5gIlmG7xkwxVHHJ8KCA==} + + remark-lint-heading-style@4.0.0: + resolution: {integrity: sha512-dQ6Jul5K0+aNUvrq4W7H0+osSoC9hsmwHZqBFq000+eMP/hWJqI8tuudw1rap8HHYuOsKLRbB5q+Fr7G+3Vw+Q==} + + remark-lint-link-title-style@4.0.0: + resolution: {integrity: sha512-cihTO5dkhjMj/evYIDAvRdQHD82OQeF4fNAq8FLb81HmFKo77VlSF6CK55H1bvlZogfJG58uN/5d1tSsOdcEbg==} + + remark-lint-list-item-content-indent@4.0.0: + resolution: {integrity: sha512-L4GZgWQQ54qWKbnDle3dbEOtnq+qdmZJ70lpM3yMFEMHs4xejqPKsIoiYeUtIV0rYHHCWS7IlLzcgYUK9vENQw==} + + remark-lint-no-dead-urls@1.1.0: + resolution: {integrity: sha512-it3EZmMQ+hwGhUf60NkXN0mMIFuFkS0cxdbgEbhZ/Fj1PlUBpe3gDBtWJ/sqNwSNvQlNSzpvMQkNHSoAhlsVjA==} + engines: {node: '>=6'} + + remark-lint-ordered-list-marker-style@4.0.0: + resolution: {integrity: sha512-xZ7Xppy5fzACH4b9h1b4lTzVtNY2AlUkNTfl1Oe6cIKN8tk3juFxN0wL2RpktPtSZ7iRIabzFmg6l8WPhlASJA==} + + remark-lint-ordered-list-marker-value@4.0.0: + resolution: {integrity: sha512-7UjNU2Nv9LGEONTU9GPmTVoNoGKD5aL1X2xHzMbSJiTc50bfcazYqZawO+qj1pQ04WPhto1qHnl0HRB5wwSVwA==} + + remark-lint-rule-style@4.0.0: + resolution: {integrity: sha512-Kt7IHMB5IbLgRFKaFUmB895sV3PTD0MBgN9CvXKxr1wHFF43S6tabjFIBSoQqyJRlhH0S3rK6Lvopofa009gLg==} + + remark-lint-strong-marker@4.0.0: + resolution: {integrity: sha512-YcvuzakYhQWdCH+1E30sUY+wyvq+PNa77NZAMAYO/cS/pZczFB+q4Ccttw4Q+No/chX8oMfe0GYtm8dDWLei/g==} + + remark-lint-table-cell-padding@5.0.0: + resolution: {integrity: sha512-LNyiHDQZBIOqcQGG1tYsZHW7g0v8OyRmRgDrD5WEsMaAYfM6EiECUokN/Q4py9h4oM/2KUSrdZbtfuZmy87/kA==} + + remark-lint@10.0.0: + resolution: {integrity: sha512-E8yHHDOJ8b+qI0G49BRu24pe8t0fNNBWv8ENQJpCGNrVeTeyBIGEbaUe1yuF7OG8faA6PVpcN/pqWjzW9fcBWQ==} + + remark-message-control@8.0.0: + resolution: {integrity: sha512-brpzOO+jdyE/mLqvqqvbogmhGxKygjpCUCG/PwSCU43+JZQ+RM+sSzkCWBcYvgF3KIAVNIoPsvXjBkzO7EdsYQ==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-preset-lint-consistent@6.0.0: + resolution: {integrity: sha512-W3fwxajdietwjnFyTH5x2le63hxWGVOXxIs7KjRqU+5wkkN6ZQyuwPeeomblmS9wQr50fkidhXNHNDyCXtqgxQ==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark-validate-links@13.0.1: + resolution: {integrity: sha512-GWDZWJAQU0+Fsm1GCLNeJoVcE9L3XTVrWCgQZOYREfXqRFIYaSoIBbARZizLm/vBESq+a3GwEBnIflSCNw26tw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + + request-progress@3.0.0: + resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + responselike@1.0.2: + resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + + rollup@4.21.0: + resolution: {integrity: sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + run-con@1.3.2: + resolution: {integrity: sha512-CcfE+mYiTcKEzg0IqS08+efdnH0oJ3zV0wSUFBNrMHMuxCtXvBCLzCJHatwuXDcu/RlhjTziTo/a1ruQik6/Yg==} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-filename@1.6.3: + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + + sass@1.77.8: + resolution: {integrity: sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + secure-keys@1.0.0: + resolution: {integrity: sha512-nZi59hW3Sl5P3+wOO89eHBAAGwmCPd2aE1+dLZV5MO+ItQctIvAqihzaAXIQhvtH4KJPxM080HsnqltR2y8cWg==} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + sliced@1.0.1: + resolution: {integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + smol-toml@1.2.2: + resolution: {integrity: sha512-fVEjX2ybKdJKzFL46VshQbj9PuA4IUKivalgp48/3zwS9vXzyykzQ6AX92UxHSvWJagziMRLeHMgEzoGO7A8hQ==} + engines: {node: '>= 18'} + + source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.20: + resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} + + split@0.3.3: + resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + start-server-and-test@2.0.5: + resolution: {integrity: sha512-2CV4pz69NJVJKQmJeSr+O+SPtOreu0yxvhPmSXclzmAKkPREuMabyMh+Txpzemjx0RDzXOcG2XkhiUuxjztSQw==} + engines: {node: '>=16'} + hasBin: true + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + + stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + + stream-combiner@0.0.4: + resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@6.1.0: + resolution: {integrity: sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==} + engines: {node: '>=16'} + + string.prototype.includes@2.0.0: + resolution: {integrity: sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==} + + string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-color@9.4.0: + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + systemjs@6.15.1: + resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + terser@5.31.6: + resolution: {integrity: sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==} + engines: {node: '>=10'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + throttleit@1.0.1: + resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-typed-emitter@2.1.0: + resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinypool@1.0.1: + resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} + engines: {node: '>=14.0.0'} + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-readable-stream@1.0.0: + resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} + engines: {node: '>=6'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tosource@2.0.0-alpha.3: + resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} + engines: {node: '>=10'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.3.1: + resolution: {integrity: sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==} + engines: {node: '>=6'} + + type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unified-args@11.0.1: + resolution: {integrity: sha512-WEQghE91+0s3xPVs0YW6a5zUduNLjmANswX7YbBfksHNDGMjHxaWCql4SR7c9q0yov/XiIEdk6r/LqfPjaYGcw==} + + unified-engine@11.2.1: + resolution: {integrity: sha512-xBAdZ8UY2X4R9Hm6X6kMne4Nz0PlpOc1oE6DPeqJnewr5Imkb8uT5Eyvy1h7xNekPL3PSWh3ZJyNrMW6jnNQBg==} + + unified-lint-rule@1.0.6: + resolution: {integrity: sha512-YPK15YBFwnsVorDFG/u0cVVQN5G2a3V8zv5/N6KN3TCG+ajKtaALcy7u14DCSrJI+gZeyYquFL9cioJXOGXSvg==} + + unified-lint-rule@3.0.0: + resolution: {integrity: sha512-Sz96ILLsTy3djsG3H44zFb2b77MFf9CQVYnV3PWkxgRX8/n31fFrr+JnzUaJ6cbOHTtZnL1A71+YodsTjzwAew==} + + unified-message-control@5.0.0: + resolution: {integrity: sha512-B2cSAkpuMVVmPP90KCfKdBhm1e9KYJ+zK3x5BCa0N65zpq1Ybkc9C77+M5qwR8FWO7RF3LM5QRRPZtgjW6DUCw==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-inspect@8.1.0: + resolution: {integrity: sha512-mOlg8Mp33pR0eeFpo5d2902ojqFFOKMMG2hF8bmH7ZlhnmjFgh0NI3/ZDwdaBJNbvrS7LZFVrBVtIE9KZ9s7vQ==} + + unist-util-is@4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@2.0.3: + resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + + unzip-crx-3@0.2.0: + resolution: {integrity: sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ==} + + update-browserslist-db@1.1.0: + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse-lax@3.0.0: + resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} + engines: {node: '>=4'} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + + verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile-reporter@8.1.1: + resolution: {integrity: sha512-qxRZcnFSQt6pWKn3PAk81yLK2rO2i7CDXpy8v8ZquiEOMLSnPw6BMSi9Y1sUCwGGl7a9b3CJT1CKpnRF7pp66g==} + + vfile-sort@4.0.0: + resolution: {integrity: sha512-lffPI1JrbHDTToJwcq0rl6rBmkjQmMuXkAxsZPRS9DXbaJQvc642eCg6EGxcX2i1L+esbuhq+2l9tBll5v8AeQ==} + + vfile-statistics@3.0.0: + resolution: {integrity: sha512-/qlwqwWBWFOmpXujL/20P+Iuydil0rZZNglR+VNm6J0gpLHwuVM5s7g2TfVoswbXjZ4HuIhLMySEyIw5i7/D8w==} + + vfile@6.0.2: + resolution: {integrity: sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==} + + vite-node@2.0.5: + resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.2: + resolution: {integrity: sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.0.5: + resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.0.5 + '@vitest/ui': 2.0.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + vue-component-type-helpers@2.0.29: + resolution: {integrity: sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-tsc@2.0.29: + resolution: {integrity: sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.4.38: + resolution: {integrity: sha512-f0ZgN+mZ5KFgVv9wz0f4OgVKukoXtS3nwET4c2vLBGQR50aI8G0cqbFtLlX9Yiyg3LFGBitruPHt2PxwTduJEw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + wait-on@7.2.0: + resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} + engines: {node: '>=12.0.0'} + hasBin: true + + walk-up-path@3.0.1: + resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + + which-builtin-type@1.1.4: + resolution: {integrity: sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrapped@1.0.1: + resolution: {integrity: sha512-ZTKuqiTu3WXtL72UKCCnQLRax2IScKH7oQ+mvjbpvNE+NJxIWIemDqqM2GxNr4N16NCjOYpIgpin5pStM7kM5g==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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 + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaku@0.16.7: + resolution: {integrity: sha512-Syu3IB3rZvKvYk7yTiyl1bo/jiEFaaStrgv1V2TIJTqYPStSMQVO8EQjg/z+DRzLq/4LIIharNT3iH1hylEIRw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml-lint@1.7.0: + resolution: {integrity: sha512-zeBC/kskKQo4zuoGQ+IYjw6C9a/YILr2SXoEZA9jM0COrSwvwVbfTiFegT8qYBSBgOwLMWGL8sY137tOmFXGnQ==} + hasBin: true + + yaml@2.5.0: + resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + 7zip-bin@5.2.0: {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/compat-data@7.25.4': {} + + '@babel/core@7.25.2': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.5 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helpers': 7.25.0 + '@babel/parser': 7.25.4 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.4 + '@babel/types': 7.25.4 + convert-source-map: 2.0.0 + debug: 4.3.6(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.25.5': + dependencies: + '@babel/types': 7.25.4 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + '@babel/helper-annotate-as-pure@7.24.7': + dependencies: + '@babel/types': 7.25.4 + + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': + dependencies: + '@babel/traverse': 7.25.4 + '@babel/types': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-compilation-targets@7.25.2': + dependencies: + '@babel/compat-data': 7.25.4 + '@babel/helper-validator-option': 7.24.8 + browserslist: 4.23.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.25.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.24.8 + '@babel/helper-optimise-call-expression': 7.24.7 + '@babel/helper-replace-supers': 7.25.0(@babel/core@7.25.2) + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/traverse': 7.25.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.25.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + regexpu-core: 5.3.2 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + debug: 4.3.6(supports-color@8.1.1) + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.24.8': + dependencies: + '@babel/traverse': 7.25.4 + '@babel/types': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.24.7': + dependencies: + '@babel/traverse': 7.25.4 + '@babel/types': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + '@babel/traverse': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.24.7': + dependencies: + '@babel/types': 7.25.4 + + '@babel/helper-plugin-utils@7.24.8': {} + + '@babel/helper-remap-async-to-generator@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-wrap-function': 7.25.0 + '@babel/traverse': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-member-expression-to-functions': 7.24.8 + '@babel/helper-optimise-call-expression': 7.24.7 + '@babel/traverse': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-simple-access@7.24.7': + dependencies: + '@babel/traverse': 7.25.4 + '@babel/types': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.24.7': + dependencies: + '@babel/traverse': 7.25.4 + '@babel/types': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.24.8': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/helper-validator-option@7.24.8': {} + + '@babel/helper-wrap-function@7.25.0': + dependencies: + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.4 + '@babel/types': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.25.0': + dependencies: + '@babel/template': 7.25.0 + '@babel/types': 7.25.4 + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@babel/parser@7.25.4': + dependencies: + '@babel/types': 7.25.4 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/traverse': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-transform-optional-chaining': 7.24.8(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/traverse': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-async-generator-functions@7.25.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-remap-async-to-generator': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/traverse': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-remap-async-to-generator': 7.25.0(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-block-scoping@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-class-properties@7.25.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.25.4(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.25.4(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.25.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-replace-supers': 7.25.0(@babel/core@7.25.2) + '@babel/traverse': 7.25.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/template': 7.25.0 + + '@babel/plugin-transform-destructuring@7.24.8(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-duplicate-keys@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-dynamic-import@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-exponentiation-operator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.25.1(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/traverse': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-literals@7.25.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + + '@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-modules-amd@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.24.8(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-simple-access': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + '@babel/traverse': 7.25.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-new-target@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-nullish-coalescing-operator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-numeric-separator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + + '@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.25.2) + + '@babel/plugin-transform-object-super@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-replace-supers': 7.25.0(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-optional-chaining@7.24.8(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-private-methods@7.25.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.25.4(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.25.4(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-reserved-words@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-spread@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-typeof-symbol@7.24.8(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-unicode-property-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-unicode-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-unicode-sets-regex@7.25.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/preset-env@7.25.4(@babel/core@7.25.2)': + dependencies: + '@babel/compat-data': 7.25.4 + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-validator-option': 7.24.8 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.2) + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-async-generator-functions': 7.25.4(@babel/core@7.25.2) + '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoping': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-transform-class-properties': 7.25.4(@babel/core@7.25.2) + '@babel/plugin-transform-class-static-block': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-classes': 7.25.4(@babel/core@7.25.2) + '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-destructuring': 7.24.8(@babel/core@7.25.2) + '@babel/plugin-transform-dotall-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-duplicate-keys': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-transform-dynamic-import': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-exponentiation-operator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-function-name': 7.25.1(@babel/core@7.25.2) + '@babel/plugin-transform-json-strings': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-literals': 7.25.2(@babel/core@7.25.2) + '@babel/plugin-transform-logical-assignment-operators': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-modules-amd': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-modules-commonjs': 7.24.8(@babel/core@7.25.2) + '@babel/plugin-transform-modules-systemjs': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-transform-modules-umd': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-new-target': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-numeric-separator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-optional-catch-binding': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-optional-chaining': 7.24.8(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-private-methods': 7.25.4(@babel/core@7.25.2) + '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-regenerator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-reserved-words': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-typeof-symbol': 7.24.8(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-escapes': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-property-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-sets-regex': 7.25.4(@babel/core@7.25.2) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.2) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.25.2) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.25.2) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.25.2) + core-js-compat: 3.38.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/types': 7.25.4 + esutils: 2.0.3 + + '@babel/regjsgen@0.8.0': {} + + '@babel/runtime@7.25.4': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.0': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.25.4 + '@babel/types': 7.25.4 + + '@babel/traverse@7.25.4': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.5 + '@babel/parser': 7.25.4 + '@babel/template': 7.25.0 + '@babel/types': 7.25.4 + debug: 4.3.6(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.25.4': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@colors/colors@1.5.0': + optional: true + + '@cypress/request@3.0.1': + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.1 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + http-signature: 1.3.6 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.10.4 + safe-buffer: 5.2.1 + tough-cookie: 4.1.4 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + + '@cypress/xvfb@1.2.4(supports-color@8.1.1)': + dependencies: + debug: 3.2.7(supports-color@8.1.1) + lodash.once: 4.1.1 + transitivePeerDependencies: + - supports-color + + '@develar/schema-utils@2.6.5': + dependencies: + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + '@electron/asar@3.2.10': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.2 + + '@electron/get@2.0.3': + dependencies: + debug: 4.3.6(supports-color@8.1.1) + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.2.1': + dependencies: + debug: 4.3.6(supports-color@8.1.1) + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.0.5': + dependencies: + compare-version: 0.1.2 + debug: 4.3.6(supports-color@8.1.1) + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/universal@1.5.1': + dependencies: + '@electron/asar': 3.2.10 + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.3.6(supports-color@8.1.1) + dir-compare: 3.3.0 + fs-extra: 9.1.0 + minimatch: 3.1.2 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.0': {} + + '@eslint-types/import@2.29.0-1': {} + + '@eslint-types/typescript-eslint@6.21.0': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.3.6(supports-color@8.1.1) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.0': {} + + '@floating-ui/core@1.6.7': + dependencies: + '@floating-ui/utils': 0.2.7 + + '@floating-ui/dom@1.6.10': + dependencies: + '@floating-ui/core': 1.6.7 + '@floating-ui/utils': 0.2.7 + + '@floating-ui/utils@0.2.7': {} + + '@floating-ui/vue@1.1.4(vue@3.4.38(typescript@5.5.4))': + dependencies: + '@floating-ui/dom': 1.6.10 + '@floating-ui/utils': 0.2.7 + vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.6(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@juggle/resize-observer@3.4.0': {} + + '@leichtgewicht/ip-codec@2.0.5': {} + + '@malept/cross-spawn-promise@1.1.1': + dependencies: + cross-spawn: 7.0.3 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.3.6(supports-color@8.1.1) + fs-extra: 9.1.0 + lodash: 4.17.21 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@modyfi/vite-plugin-yaml@1.1.0(rollup@4.21.0)(vite@5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6))': + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.21.0) + js-yaml: 4.1.0 + tosource: 2.0.0-alpha.3 + vite: 5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6) + transitivePeerDependencies: + - rollup + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@npmcli/config@8.3.4': + dependencies: + '@npmcli/map-workspaces': 3.0.6 + '@npmcli/package-json': 5.2.0 + ci-info: 4.0.0 + ini: 4.1.3 + nopt: 7.2.1 + proc-log: 4.2.0 + semver: 7.6.3 + walk-up-path: 3.0.1 + transitivePeerDependencies: + - bluebird + + '@npmcli/git@5.0.8': + dependencies: + '@npmcli/promise-spawn': 7.0.2 + ini: 4.1.3 + lru-cache: 10.4.3 + npm-pick-manifest: 9.1.0 + proc-log: 4.2.0 + promise-inflight: 1.0.1 + promise-retry: 2.0.1 + semver: 7.6.3 + which: 4.0.0 + transitivePeerDependencies: + - bluebird + + '@npmcli/map-workspaces@3.0.6': + dependencies: + '@npmcli/name-from-folder': 2.0.0 + glob: 10.4.5 + minimatch: 9.0.5 + read-package-json-fast: 3.0.2 + + '@npmcli/name-from-folder@2.0.0': {} + + '@npmcli/package-json@5.2.0': + dependencies: + '@npmcli/git': 5.0.8 + glob: 10.4.5 + hosted-git-info: 7.0.2 + json-parse-even-better-errors: 3.0.2 + normalize-package-data: 6.0.2 + proc-log: 4.2.0 + semver: 7.6.3 + transitivePeerDependencies: + - bluebird + + '@npmcli/promise-spawn@7.0.2': + dependencies: + which: 4.0.0 + + '@one-ini/wasm@0.1.1': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/pluginutils@5.1.0(rollup@4.21.0)': + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + optionalDependencies: + rollup: 4.21.0 + + '@rollup/rollup-android-arm-eabi@4.21.0': + optional: true + + '@rollup/rollup-android-arm64@4.21.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.21.0': + optional: true + + '@rollup/rollup-darwin-x64@4.21.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.21.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.21.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.21.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.21.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.21.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.21.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.21.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.21.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.21.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.21.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.21.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.21.0': + optional: true + + '@rushstack/eslint-patch@1.10.4': {} + + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + + '@sindresorhus/is@0.14.0': {} + + '@sindresorhus/is@4.6.0': {} + + '@szmarczak/http-timer@1.1.2': + dependencies: + defer-to-connect: 1.1.3 + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@tootallnate/once@2.0.0': {} + + '@types/ace@0.0.52': {} + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 20.16.1 + '@types/responselike': 1.0.3 + + '@types/concat-stream@2.0.3': + dependencies: + '@types/node': 20.16.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.5 + + '@types/estree@1.0.5': {} + + '@types/file-saver@2.0.7': {} + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 22.5.0 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/hosted-git-info@3.0.5': {} + + '@types/http-cache-semantics@4.0.4': {} + + '@types/is-empty@1.2.3': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.5.0 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/ms@0.7.34': {} + + '@types/node@20.16.1': + dependencies: + undici-types: 6.19.8 + + '@types/node@22.5.0': + dependencies: + undici-types: 6.19.8 + + '@types/plist@3.0.5': + dependencies: + '@types/node': 22.5.0 + xmlbuilder: 15.1.1 + optional: true + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.5.0 + + '@types/semver@7.5.8': {} + + '@types/sinonjs__fake-timers@8.1.1': {} + + '@types/sizzle@2.3.8': {} + + '@types/supports-color@8.1.3': {} + + '@types/text-table@0.2.5': {} + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/verror@1.10.10': + optional: true + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.5.0 + optional: true + + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.6(supports-color@8.1.1) + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.6(supports-color@8.1.1) + eslint: 8.57.0 + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + debug: 4.3.6(supports-color@8.1.1) + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@6.21.0': {} + + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.6(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) + eslint: 8.57.0 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.2.0': {} + + '@vitejs/plugin-legacy@5.4.2(terser@5.31.6)(vite@5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6))': + dependencies: + '@babel/core': 7.25.2 + '@babel/preset-env': 7.25.4(@babel/core@7.25.2) + browserslist: 4.23.3 + browserslist-to-esbuild: 2.1.1(browserslist@4.23.3) + core-js: 3.38.1 + magic-string: 0.30.11 + regenerator-runtime: 0.14.1 + systemjs: 6.15.1 + terser: 5.31.6 + vite: 5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue@5.1.2(vite@5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6))(vue@3.4.38(typescript@5.5.4))': + dependencies: + vite: 5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6) + vue: 3.4.38(typescript@5.5.4) + + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.0.5': + dependencies: + '@vitest/utils': 2.0.5 + pathe: 1.1.2 + + '@vitest/snapshot@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + magic-string: 0.30.11 + pathe: 1.1.2 + + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.0 + + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.1.1 + tinyrainbow: 1.2.0 + + '@volar/language-core@2.4.0': + dependencies: + '@volar/source-map': 2.4.0 + + '@volar/source-map@2.4.0': {} + + '@volar/typescript@2.4.0': + dependencies: + '@volar/language-core': 2.4.0 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + + '@vue/compiler-core@3.4.38': + dependencies: + '@babel/parser': 7.25.4 + '@vue/shared': 3.4.38 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.0 + + '@vue/compiler-dom@3.4.38': + dependencies: + '@vue/compiler-core': 3.4.38 + '@vue/shared': 3.4.38 + + '@vue/compiler-sfc@3.4.38': + dependencies: + '@babel/parser': 7.25.4 + '@vue/compiler-core': 3.4.38 + '@vue/compiler-dom': 3.4.38 + '@vue/compiler-ssr': 3.4.38 + '@vue/shared': 3.4.38 + estree-walker: 2.0.2 + magic-string: 0.30.11 + postcss: 8.4.41 + source-map-js: 1.2.0 + + '@vue/compiler-ssr@3.4.38': + dependencies: + '@vue/compiler-dom': 3.4.38 + '@vue/shared': 3.4.38 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/eslint-config-airbnb-with-typescript@8.0.0(eslint-plugin-vue@9.27.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@eslint-types/import': 2.29.0-1 + '@eslint-types/typescript-eslint': 6.21.0 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@vue/eslint-config-airbnb': 8.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint-plugin-vue@9.27.0(eslint@8.57.0))(eslint@8.57.0) + eslint: 8.57.0 + eslint-config-airbnb-typescript: 17.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0) + eslint-define-config: 2.1.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-vue: 9.27.0(eslint@8.57.0) + typescript: 5.5.4 + vue-eslint-parser: 9.4.3(eslint@8.57.0) + transitivePeerDependencies: + - eslint-import-resolver-webpack + - supports-color + + '@vue/eslint-config-airbnb@8.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint-plugin-vue@9.27.0(eslint@8.57.0))(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-custom-alias: 1.3.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)) + eslint-import-resolver-node: 0.3.9 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) + eslint-plugin-react: 7.35.0(eslint@8.57.0) + eslint-plugin-vue: 9.27.0(eslint@8.57.0) + eslint-plugin-vuejs-accessibility: 2.4.1(eslint@8.57.0) + vue-eslint-parser: 9.4.3(eslint@8.57.0) + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + '@vue/eslint-config-typescript@12.0.0(eslint-plugin-vue@9.27.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + eslint: 8.57.0 + eslint-plugin-vue: 9.27.0(eslint@8.57.0) + vue-eslint-parser: 9.4.3(eslint@8.57.0) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@vue/language-core@2.0.29(typescript@5.5.4)': + dependencies: + '@volar/language-core': 2.4.0 + '@vue/compiler-dom': 3.4.38 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.4.38 + computeds: 0.0.1 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.5.4 + + '@vue/reactivity@3.4.38': + dependencies: + '@vue/shared': 3.4.38 + + '@vue/runtime-core@3.4.38': + dependencies: + '@vue/reactivity': 3.4.38 + '@vue/shared': 3.4.38 + + '@vue/runtime-dom@3.4.38': + dependencies: + '@vue/reactivity': 3.4.38 + '@vue/runtime-core': 3.4.38 + '@vue/shared': 3.4.38 + csstype: 3.1.3 + + '@vue/server-renderer@3.4.38(vue@3.4.38(typescript@5.5.4))': + dependencies: + '@vue/compiler-ssr': 3.4.38 + '@vue/shared': 3.4.38 + vue: 3.4.38(typescript@5.5.4) + + '@vue/shared@3.4.38': {} + + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.1 + vue-component-type-helpers: 2.0.29 + + '@xmldom/xmldom@0.8.10': {} + + abbrev@2.0.0: {} + + ace-builds@1.36.0: {} + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.1: + dependencies: + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + app-builder-bin@4.0.0: {} + + app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): + dependencies: + '@develar/schema-utils': 2.6.5 + '@electron/notarize': 2.2.1 + '@electron/osx-sign': 1.0.5 + '@electron/universal': 1.5.1 + '@malept/flatpak-bundler': 0.4.0 + '@types/fs-extra': 9.0.13 + async-exit-hook: 2.0.1 + bluebird-lst: 1.0.9 + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + chromium-pickle-js: 0.2.0 + debug: 4.3.6(supports-color@8.1.1) + dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) + ejs: 3.1.10 + electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3) + electron-publish: 24.13.1 + form-data: 4.0.0 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + is-ci: 3.0.1 + isbinaryfile: 5.0.2 + js-yaml: 4.1.0 + lazy-val: 1.0.5 + minimatch: 5.1.6 + read-config-file: 6.3.2 + sanitize-filename: 1.6.3 + semver: 7.6.3 + tar: 6.2.1 + temp-file: 3.4.0 + transitivePeerDependencies: + - supports-color + + arch@2.2.0: {} + + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + + array-union@2.1.0: {} + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.findlastindex@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.flat@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + + array.prototype.flatmap@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + + array.prototype.tosorted@1.1.4: + 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 + + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + astral-regex@2.0.0: {} + + async-exit-hook@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + autoprefixer@10.4.20(postcss@8.4.41): + dependencies: + browserslist: 4.23.3 + caniuse-lite: 1.0.30001651 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.4.41 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + aws-sign2@0.7.0: {} + + aws4@1.13.1: {} + + axe-core@4.10.0: {} + + axios@1.7.5(debug@4.3.6): + dependencies: + follow-redirects: 1.15.6(debug@4.3.6) + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axobject-query@3.1.1: + dependencies: + deep-equal: 2.2.3 + + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.25.2): + dependencies: + '@babel/compat-data': 7.25.4 + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.2) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.25.2): + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.2) + core-js-compat: 3.38.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.25.2): + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + blob-util@2.0.2: {} + + bluebird-lst@1.0.9: + dependencies: + bluebird: 3.7.2 + + bluebird@3.7.2: {} + + boolbase@1.0.0: {} + + boolean@3.2.0: + optional: true + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist-to-esbuild@2.1.1(browserslist@4.23.3): + dependencies: + browserslist: 4.23.3 + meow: 13.2.0 + + browserslist@4.23.3: + dependencies: + caniuse-lite: 1.0.30001651 + electron-to-chromium: 1.5.13 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) + + buffer-crc32@0.2.13: {} + + buffer-equal@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builder-util-runtime@9.2.4: + dependencies: + debug: 4.3.6(supports-color@8.1.1) + sax: 1.4.1 + transitivePeerDependencies: + - supports-color + + builder-util@24.13.1: + dependencies: + 7zip-bin: 5.2.0 + '@types/debug': 4.1.12 + app-builder-bin: 4.0.0 + bluebird-lst: 1.0.9 + builder-util-runtime: 9.2.4 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.6(supports-color@8.1.1) + fs-extra: 10.1.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-ci: 3.0.1 + js-yaml: 4.1.0 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + transitivePeerDependencies: + - supports-color + + cac@6.7.14: {} + + cacheable-lookup@5.0.4: {} + + cacheable-request@6.1.0: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 3.1.0 + lowercase-keys: 2.0.0 + normalize-url: 4.5.1 + responselike: 1.0.2 + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + cachedir@2.4.0: {} + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001651: {} + + caseless@0.12.0: {} + + chai@5.1.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + character-entities@2.0.2: {} + + check-error@2.1.1: {} + + check-links@1.1.8: + dependencies: + got: 9.6.0 + is-relative-url: 2.0.0 + p-map: 2.1.0 + p-memoize: 2.1.0 + + check-more-types@2.24.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@2.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@3.9.0: {} + + ci-info@4.0.0: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + co@3.1.0: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@10.0.1: {} + + commander@12.1.0: {} + + commander@2.20.3: {} + + commander@5.1.0: {} + + commander@6.2.1: {} + + common-tags@1.8.2: {} + + compare-version@0.1.2: {} + + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + computeds@0.0.1: {} + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + config-file-ts@0.2.6: + dependencies: + glob: 10.4.5 + typescript: 5.5.4 + + confusing-browser-globals@1.0.11: {} + + consola@2.15.3: {} + + convert-source-map@2.0.0: {} + + core-js-compat@3.38.1: + dependencies: + browserslist: 4.23.3 + + core-js@3.38.1: {} + + core-util-is@1.0.2: {} + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + crc@3.8.0: + dependencies: + buffer: 5.7.1 + optional: true + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + cssstyle@4.0.1: + dependencies: + rrweb-cssom: 0.6.0 + + csstype@3.1.3: {} + + cypress@13.13.3: + dependencies: + '@cypress/request': 3.0.1 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.8 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.4.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.5 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.13 + debug: 4.3.6(supports-color@8.1.1) + enquirer: 2.4.1 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.4.1) + lodash: 4.17.21 + log-symbols: 4.1.0 + minimist: 1.2.8 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + process: 0.11.10 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + semver: 7.6.3 + supports-color: 8.1.1 + tmp: 0.2.3 + untildify: 4.0.0 + yauzl: 2.10.0 + + damerau-levenshtein@1.0.8: {} + + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + dayjs@1.11.13: {} + + de-indent@1.0.2: {} + + debug@3.2.7(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + debug@4.3.6(supports-color@8.1.1): + dependencies: + ms: 2.1.2 + optionalDependencies: + supports-color: 8.1.1 + + decimal.js@10.4.3: {} + + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + + decompress-response@3.3.0: + dependencies: + mimic-response: 1.0.1 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-eql@5.0.2: {} + + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.4 + is-arguments: 1.1.1 + is-array-buffer: 3.0.4 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + side-channel: 1.0.6 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + defer-to-connect@1.1.3: {} + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + dequal@2.0.3: {} + + detect-node@2.1.0: + optional: true + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dir-compare@3.3.0: + dependencies: + buffer-equal: 1.0.1 + minimatch: 3.1.2 + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3): + dependencies: + app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + fs-extra: 10.1.0 + iconv-lite: 0.6.3 + js-yaml: 4.1.0 + optionalDependencies: + dmg-license: 1.0.11 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + dmg-license@1.0.11: + dependencies: + '@types/plist': 3.0.5 + '@types/verror': 1.10.10 + ajv: 6.12.6 + crc: 3.8.0 + iconv-corefoundation: 1.1.7 + plist: 3.1.0 + smart-buffer: 4.2.0 + verror: 1.10.1 + optional: true + + dns-packet@5.6.1: + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + + dns-socket@4.2.2: + dependencies: + dns-packet: 5.6.1 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dotenv-expand@5.1.0: {} + + dotenv@9.0.2: {} + + duplexer3@0.1.5: {} + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.6.3 + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3): + dependencies: + app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) + archiver: 5.3.2 + builder-util: 24.13.1 + fs-extra: 10.1.0 + transitivePeerDependencies: + - dmg-builder + - supports-color + + electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): + dependencies: + app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + chalk: 4.1.2 + dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) + fs-extra: 10.1.0 + is-ci: 3.0.1 + lazy-val: 1.0.5 + read-config-file: 6.3.2 + simple-update-notifier: 2.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + electron-devtools-installer@3.2.0: + dependencies: + rimraf: 3.0.2 + semver: 7.6.3 + tslib: 2.7.0 + unzip-crx-3: 0.2.0 + + electron-log@5.1.7: {} + + electron-progressbar@2.2.1: + dependencies: + extend: 3.0.2 + + electron-publish@24.13.1: + dependencies: + '@types/fs-extra': 9.0.13 + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + chalk: 4.1.2 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + + electron-to-chromium@1.5.13: {} + + electron-updater@6.2.1: + dependencies: + builder-util-runtime: 9.2.4 + fs-extra: 10.1.0 + js-yaml: 4.1.0 + lazy-val: 1.0.5 + lodash.escaperegexp: 4.1.2 + lodash.isequal: 4.5.0 + semver: 7.6.3 + tiny-typed-emitter: 2.1.0 + transitivePeerDependencies: + - supports-color + + electron-vite@2.3.0(vite@5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6)): + dependencies: + '@babel/core': 7.25.2 + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.25.2) + cac: 6.7.14 + esbuild: 0.21.5 + magic-string: 0.30.11 + picocolors: 1.0.1 + vite: 5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6) + transitivePeerDependencies: + - supports-color + + electron@31.4.0: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 20.16.1 + extract-zip: 2.0.1(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + emoji-regex@10.3.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.17.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + err-code@2.0.3: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.23.3: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.2 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + + es-iterator-helpers@1.0.19: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + globalthis: 1.0.4 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + iterator.prototype: 1.1.2 + safe-array-concat: 1.1.2 + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.0.2: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + + es6-error@4.1.1: + optional: true + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.1.2: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0): + dependencies: + confusing-browser-globals: 1.0.11 + eslint: 8.57.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + object.assign: 4.1.5 + object.entries: 1.1.8 + semver: 6.3.1 + + eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0): + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + eslint: 8.57.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + + eslint-define-config@2.1.0: {} + + eslint-import-resolver-custom-alias@1.3.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)): + dependencies: + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + glob-parent: 6.0.2 + resolve: 1.22.8 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7(supports-color@8.1.1) + is-core-module: 2.15.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + dependencies: + debug: 4.3.6(supports-color@8.1.1) + enhanced-resolve: 5.17.1 + eslint: 8.57.0 + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + fast-glob: 3.3.2 + get-tsconfig: 4.7.6 + is-core-module: 2.15.1 + is-glob: 4.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + dependencies: + debug: 3.2.7(supports-color@8.1.1) + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + transitivePeerDependencies: + - supports-color + + eslint-plugin-cypress@3.5.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + globals: 13.24.0 + + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + dependencies: + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + hasown: 2.0.2 + is-core-module: 2.15.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.9.0(eslint@8.57.0): + dependencies: + aria-query: 5.1.3 + array-includes: 3.1.8 + array.prototype.flatmap: 1.3.2 + ast-types-flow: 0.0.8 + axe-core: 4.10.0 + axobject-query: 3.1.1 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + es-iterator-helpers: 1.0.19 + eslint: 8.57.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.0.3 + string.prototype.includes: 2.0.0 + + eslint-plugin-react@7.35.0(eslint@8.57.0): + dependencies: + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.0.19 + eslint: 8.57.0 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.8 + object.fromentries: 2.0.8 + object.values: 1.2.0 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.11 + string.prototype.repeat: 1.0.0 + + eslint-plugin-vue@9.27.0(eslint@8.57.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + eslint: 8.57.0 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.6.3 + vue-eslint-parser: 9.4.3(eslint@8.57.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-vuejs-accessibility@2.4.1(eslint@8.57.0): + dependencies: + aria-query: 5.3.0 + emoji-regex: 10.3.0 + eslint: 8.57.0 + vue-eslint-parser: 9.4.3(eslint@8.57.0) + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.6(supports-color@8.1.1) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.5 + + esutils@2.0.3: {} + + event-stream@3.3.4: + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.1.0 + pause-stream: 0.0.11 + split: 0.3.3 + stream-combiner: 0.0.4 + through: 2.3.8 + + eventemitter2@6.4.7: {} + + execa@4.1.0: + dependencies: + cross-spawn: 7.0.3 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + executable@4.1.1: + dependencies: + pify: 2.3.0 + + extend@3.0.2: {} + + extract-zip@2.0.1(supports-color@8.1.1): + dependencies: + debug: 4.3.6(supports-color@8.1.1) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.3.0: {} + + extsprintf@1.4.1: + optional: true + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-saver@2.0.5: {} + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.1: {} + + follow-redirects@1.15.6(debug@4.3.6): + optionalDependencies: + debug: 4.3.6(supports-color@8.1.1) + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + forever-agent@0.6.1: {} + + form-data@2.3.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fraction.js@4.3.7: {} + + from@0.1.7: {} + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-func-name@2.0.2: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-stdin@9.0.0: {} + + get-stream@4.1.0: + dependencies: + pump: 3.0.0 + + get-stream@5.2.0: + dependencies: + pump: 3.0.0 + + get-stream@6.0.1: {} + + get-stream@8.0.1: {} + + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + + get-tsconfig@4.7.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + getos@3.2.1: + dependencies: + async: 3.2.6 + + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + + github-slugger@2.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.6.3 + serialize-error: 7.0.1 + optional: true + + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + + globals@11.12.0: {} + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + got@9.6.0: + dependencies: + '@sindresorhus/is': 0.14.0 + '@szmarczak/http-timer': 1.1.2 + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + cacheable-request: 6.1.0 + decompress-response: 3.3.0 + duplexer3: 0.1.5 + get-stream: 4.1.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 1.1.0 + to-readable-stream: 1.0.0 + url-parse-lax: 3.0.0 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.0.2: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-cache-semantics@4.1.1: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.1 + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + http-signature@1.3.6: + dependencies: + assert-plus: 1.0.0 + jsprim: 2.0.2 + sshpk: 1.18.0 + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.5: + dependencies: + agent-base: 7.1.1 + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + human-signals@1.1.1: {} + + human-signals@2.1.0: {} + + human-signals@5.0.0: {} + + iconv-corefoundation@1.1.7: + dependencies: + cli-truncate: 2.1.0 + node-addon-api: 1.7.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + immediate@3.0.6: {} + + immutable@4.3.7: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.1.0: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@2.0.0: {} + + ini@4.1.3: {} + + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + + ip-regex@4.3.0: {} + + is-absolute-url@2.1.0: {} + + is-arguments@1.1.1: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-arrayish@0.2.1: {} + + is-async-function@2.0.0: + dependencies: + has-tostringtag: 1.0.2 + + is-bigint@1.0.4: + dependencies: + has-bigints: 1.0.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.1.2: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + + is-empty@1.2.0: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + + is-ip@3.1.0: + dependencies: + ip-regex: 4.3.0 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-online@8.5.1: + dependencies: + got: 9.6.0 + p-any: 2.1.0 + p-timeout: 3.2.0 + public-ip: 4.0.4 + + is-path-inside@3.0.3: {} + + is-plain-obj@4.1.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.1.4: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-relative-url@2.0.0: + dependencies: + is-absolute-url: 2.1.0 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + + is-string@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-symbol@1.0.4: + dependencies: + has-symbols: 1.0.3 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-weakset@2.0.3: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.2: {} + + isexe@2.0.0: {} + + isexe@3.1.1: {} + + isstream@0.1.2: {} + + iterator.prototype@1.1.2: + dependencies: + define-properties: 1.2.1 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + reflect.getprototypeof: 1.0.6 + set-function-name: 2.0.2 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + js-beautify@1.15.1: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsbn@0.1.1: {} + + jsdom@24.1.1: + dependencies: + cssstyle: 4.0.1 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.12 + parse5: 7.1.2 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@0.5.0: {} + + jsesc@2.5.2: {} + + json-buffer@3.0.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@3.0.2: {} + + json-schema-traverse@0.4.1: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-safe@5.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonc-parser@3.2.1: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonpointer@5.0.1: {} + + jsprim@2.0.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.2 + object.assign: 4.1.5 + object.values: 1.2.0 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + keyv@3.1.0: + dependencies: + json-buffer: 3.0.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + lazy-ass@1.6.0: {} + + lazy-val@1.0.5: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + levenshtein-edit-distance@1.0.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lines-and-columns@2.0.4: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + listr2@3.14.0(enquirer@2.4.1): + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + optionalDependencies: + enquirer: 2.4.1 + + load-plugin@6.0.3: + dependencies: + '@npmcli/config': 8.3.4 + import-meta-resolve: 4.1.0 + transitivePeerDependencies: + - bluebird + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.debounce@4.0.8: {} + + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + + lodash.isequal@4.5.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash.union@4.6.0: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 + + lowercase-keys@1.0.1: {} + + lowercase-keys@2.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + magic-string@0.30.11: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + map-age-cleaner@0.1.3: + dependencies: + p-defer: 1.0.0 + + map-stream@0.1.0: {} + + markdown-extensions@2.0.0: {} + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdownlint-cli@0.41.0: + dependencies: + commander: 12.1.0 + get-stdin: 9.0.0 + glob: 10.4.5 + ignore: 5.3.2 + js-yaml: 4.1.0 + jsonc-parser: 3.2.1 + jsonpointer: 5.0.1 + markdownlint: 0.34.0 + minimatch: 9.0.5 + run-con: 1.3.2 + smol-toml: 1.2.2 + + markdownlint-micromark@0.1.9: {} + + markdownlint@0.34.0: + dependencies: + markdown-it: 14.1.0 + markdownlint-micromark: 0.1.9 + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + + mdast-comment-marker@3.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-mdx-expression: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-from-markdown@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-heading-style@3.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdast-util-mdx-expression@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.2 + + mdast-util-to-markdown@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-decode-string: 2.0.0 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdurl@2.0.0: {} + + mem@4.3.0: + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 2.1.0 + p-is-promise: 2.1.0 + + meow@13.2.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.0 + micromark-factory-label: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-factory-title: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-html-tag-name: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-destination@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-label@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-space@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-types: 2.0.0 + + micromark-factory-title@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-whitespace@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-character@2.1.0: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-chunked@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-classify-character@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-combine-extensions@2.0.0: + dependencies: + micromark-util-chunked: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-decode-numeric-character-reference@2.0.1: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-decode-string@2.0.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-symbol: 2.0.0 + + micromark-util-encode@2.0.0: {} + + micromark-util-html-tag-name@2.0.0: {} + + micromark-util-normalize-identifier@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-resolve-all@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-util-sanitize-uri@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + micromark-util-subtokenize@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-symbol@2.0.0: {} + + micromark-util-types@2.0.0: {} + + micromark@4.0.0: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.6(supports-color@8.1.1) + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@2.6.0: {} + + mimic-fn@1.2.0: {} + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.7: {} + + natural-compare@1.4.0: {} + + nconf@0.12.1: + dependencies: + async: 3.2.6 + ini: 2.0.0 + secure-keys: 1.0.0 + yargs: 16.2.0 + + node-addon-api@1.7.2: + optional: true + + node-releases@2.0.18: {} + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-package-data@6.0.2: + dependencies: + hosted-git-info: 7.0.2 + semver: 7.6.3 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + normalize-url@4.5.1: {} + + normalize-url@6.1.0: {} + + npm-install-checks@6.3.0: + dependencies: + semver: 7.6.3 + + npm-normalize-package-bin@3.0.1: {} + + npm-package-arg@11.0.3: + dependencies: + hosted-git-info: 7.0.2 + proc-log: 4.2.0 + semver: 7.6.3 + validate-npm-package-name: 5.0.1 + + npm-pick-manifest@9.1.0: + dependencies: + npm-install-checks: 6.3.0 + npm-normalize-package-bin: 3.0.1 + npm-package-arg: 11.0.3 + semver: 7.6.3 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nwsapi@2.2.12: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.2: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + object.entries@1.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + + object.values@1.2.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ospath@1.2.2: {} + + p-any@2.1.0: + dependencies: + p-cancelable: 2.1.1 + p-some: 4.1.0 + type-fest: 0.3.1 + + p-cancelable@1.1.0: {} + + p-cancelable@2.1.1: {} + + p-defer@1.0.0: {} + + p-finally@1.0.0: {} + + p-is-promise@2.1.0: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@2.1.0: {} + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-memoize@2.1.0: + dependencies: + mem: 4.3.0 + mimic-fn: 1.2.0 + + p-some@4.1.0: + dependencies: + aggregate-error: 3.1.0 + p-cancelable: 2.1.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + package-json-from-dist@1.0.0: {} + + pako@1.0.11: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@7.1.1: + dependencies: + '@babel/code-frame': 7.24.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 3.0.2 + lines-and-columns: 2.0.4 + type-fest: 3.13.1 + + parse5@7.1.2: + dependencies: + entities: 4.5.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathval@2.0.0: {} + + pause-stream@0.0.11: + dependencies: + through: 2.3.8 + + pend@1.2.0: {} + + performance-now@2.1.0: {} + + picocolors@1.0.1: {} + + picomatch@2.3.1: {} + + pify@2.3.0: {} + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pluralize@8.0.0: {} + + possible-typed-array-names@1.0.0: {} + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.41: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + + prelude-ls@1.2.1: {} + + prepend-http@2.0.0: {} + + pretty-bytes@5.6.0: {} + + proc-log@4.2.0: {} + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + progress@2.0.3: {} + + promise-inflight@1.0.1: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + propose@0.0.5: + dependencies: + levenshtein-edit-distance: 1.0.0 + + proto-list@1.2.4: {} + + proxy-from-env@1.0.0: {} + + proxy-from-env@1.1.0: {} + + ps-tree@1.2.0: + dependencies: + event-stream: 3.3.4 + + psl@1.9.0: {} + + public-ip@4.0.4: + dependencies: + dns-socket: 4.2.2 + got: 9.6.0 + is-ip: 3.1.0 + + pump@3.0.0: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + qs@6.10.4: + dependencies: + side-channel: 1.0.6 + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + quick-lru@5.1.1: {} + + react-is@16.13.1: {} + + read-config-file@6.3.2: + dependencies: + config-file-ts: 0.2.6 + dotenv: 9.0.2 + dotenv-expand: 5.1.0 + js-yaml: 4.1.0 + json5: 2.2.3 + lazy-val: 1.0.5 + + read-package-json-fast@3.0.2: + dependencies: + json-parse-even-better-errors: 3.0.2 + npm-normalize-package-bin: 3.0.1 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reflect.getprototypeof@1.0.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + globalthis: 1.0.4 + which-builtin-type: 1.1.4 + + regenerate-unicode-properties@10.1.1: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.14.1: {} + + regenerator-transform@0.15.2: + dependencies: + '@babel/runtime': 7.25.4 + + regexp.prototype.flags@1.5.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + regexpu-core@5.3.2: + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.1 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + + regjsparser@0.9.1: + dependencies: + jsesc: 0.5.0 + + remark-cli@12.0.1: + dependencies: + import-meta-resolve: 4.1.0 + markdown-extensions: 2.0.0 + remark: 15.0.1 + unified-args: 11.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + remark-lint-blockquote-indentation@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-phrasing: 4.1.0 + pluralize: 8.0.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + + remark-lint-checkbox-character-style@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-phrasing: 4.1.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-code-block-style@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-phrasing: 4.1.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-emphasis-marker@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-fenced-code-marker@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-phrasing: 4.1.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-heading-style@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-heading-style: 3.0.0 + mdast-util-phrasing: 4.1.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-link-title-style@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-list-item-content-indent@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-phrasing: 4.1.0 + pluralize: 8.0.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-no-dead-urls@1.1.0: + dependencies: + check-links: 1.1.8 + is-online: 8.5.1 + unified-lint-rule: 1.0.6 + unist-util-visit: 2.0.3 + + remark-lint-ordered-list-marker-style@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-phrasing: 4.1.0 + micromark-util-character: 2.1.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-ordered-list-marker-value@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-phrasing: 4.1.0 + micromark-util-character: 2.1.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-rule-style@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-phrasing: 4.1.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-strong-marker@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint-table-cell-padding@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + mdast-util-phrasing: 4.1.0 + pluralize: 8.0.0 + unified-lint-rule: 3.0.0 + unist-util-position: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile-message: 4.0.2 + + remark-lint@10.0.0: + dependencies: + '@types/mdast': 4.0.4 + remark-message-control: 8.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-message-control@8.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-comment-marker: 3.0.0 + unified-message-control: 5.0.0 + vfile: 6.0.2 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.1 + micromark-util-types: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-preset-lint-consistent@6.0.0: + dependencies: + remark-lint: 10.0.0 + remark-lint-blockquote-indentation: 4.0.0 + remark-lint-checkbox-character-style: 5.0.0 + remark-lint-code-block-style: 4.0.0 + remark-lint-emphasis-marker: 4.0.0 + remark-lint-fenced-code-marker: 4.0.0 + remark-lint-heading-style: 4.0.0 + remark-lint-link-title-style: 4.0.0 + remark-lint-list-item-content-indent: 4.0.0 + remark-lint-ordered-list-marker-style: 4.0.0 + remark-lint-ordered-list-marker-value: 4.0.0 + remark-lint-rule-style: 4.0.0 + remark-lint-strong-marker: 4.0.0 + remark-lint-table-cell-padding: 5.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.0 + unified: 11.0.5 + + remark-validate-links@13.0.1: + dependencies: + '@types/hosted-git-info': 3.0.5 + '@types/mdast': 4.0.4 + github-slugger: 2.0.0 + hosted-git-info: 7.0.2 + mdast-util-to-hast: 13.2.0 + mdast-util-to-string: 4.0.0 + propose: 0.0.5 + trough: 2.2.0 + unified-engine: 11.2.1 + unist-util-visit: 5.0.0 + vfile: 6.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + request-progress@3.0.0: + dependencies: + throttleit: 1.0.1 + + require-directory@2.1.1: {} + + requires-port@1.0.0: {} + + resolve-alpn@1.2.1: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + responselike@1.0.2: + dependencies: + lowercase-keys: 1.0.1 + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + reusify@1.0.4: {} + + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + + rollup@4.21.0: + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.21.0 + '@rollup/rollup-android-arm64': 4.21.0 + '@rollup/rollup-darwin-arm64': 4.21.0 + '@rollup/rollup-darwin-x64': 4.21.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.21.0 + '@rollup/rollup-linux-arm-musleabihf': 4.21.0 + '@rollup/rollup-linux-arm64-gnu': 4.21.0 + '@rollup/rollup-linux-arm64-musl': 4.21.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.21.0 + '@rollup/rollup-linux-riscv64-gnu': 4.21.0 + '@rollup/rollup-linux-s390x-gnu': 4.21.0 + '@rollup/rollup-linux-x64-gnu': 4.21.0 + '@rollup/rollup-linux-x64-musl': 4.21.0 + '@rollup/rollup-win32-arm64-msvc': 4.21.0 + '@rollup/rollup-win32-ia32-msvc': 4.21.0 + '@rollup/rollup-win32-x64-msvc': 4.21.0 + fsevents: 2.3.3 + + rrweb-cssom@0.6.0: {} + + rrweb-cssom@0.7.1: {} + + run-con@1.3.2: + dependencies: + deep-extend: 0.6.0 + ini: 4.1.3 + minimist: 1.2.8 + strip-json-comments: 3.1.1 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.1: + dependencies: + tslib: 2.7.0 + + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + + safer-buffer@2.1.2: {} + + sanitize-filename@1.6.3: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sass@1.77.8: + dependencies: + chokidar: 3.6.0 + immutable: 4.3.7 + source-map-js: 1.2.0 + + sax@1.4.1: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + secure-keys@1.0.0: {} + + semver-compare@1.0.0: + optional: true + + semver@6.3.1: {} + + semver@7.6.3: {} + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + + set-function-length@1.2.2: + 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 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + setimmediate@1.0.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.6.3 + + slash@3.0.0: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + sliced@1.0.1: {} + + smart-buffer@4.2.0: + optional: true + + smol-toml@1.2.2: {} + + source-map-js@1.2.0: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + space-separated-tokens@2.0.2: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.20 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.20 + + spdx-license-ids@3.0.20: {} + + split@0.3.3: + dependencies: + through: 2.3.8 + + sprintf-js@1.1.3: + optional: true + + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + stackback@0.0.2: {} + + start-server-and-test@2.0.5: + dependencies: + arg: 5.0.2 + bluebird: 3.7.2 + check-more-types: 2.24.0 + debug: 4.3.6(supports-color@8.1.1) + execa: 5.1.1 + lazy-ass: 1.6.0 + ps-tree: 1.2.0 + wait-on: 7.2.0(debug@4.3.6) + transitivePeerDependencies: + - supports-color + + stat-mode@1.0.0: {} + + std-env@3.7.0: {} + + stop-iteration-iterator@1.0.0: + dependencies: + internal-slot: 1.0.7 + + stream-combiner@0.0.4: + dependencies: + duplexer: 0.1.2 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@6.1.0: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 10.3.0 + strip-ansi: 7.1.0 + + string.prototype.includes@2.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.3 + + string.prototype.matchall@4.0.11: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.2 + set-function-name: 2.0.2 + side-channel: 1.0.6 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.3 + + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + + strip-bom@3.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + sumchecker@3.0.1: + dependencies: + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-color@9.4.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + systemjs@6.15.1: {} + + tapable@2.2.1: {} + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + terser@5.31.6: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-table@0.2.0: {} + + throttleit@1.0.1: {} + + through@2.3.8: {} + + tiny-typed-emitter@2.1.0: {} + + tinybench@2.9.0: {} + + tinypool@1.0.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.0: {} + + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.3 + + tmp@0.2.3: {} + + to-fast-properties@2.0.0: {} + + to-readable-stream@1.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tosource@2.0.0-alpha.3: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + + ts-api-utils@1.3.0(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.7.0: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tweetnacl@0.14.5: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.13.1: + optional: true + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@0.3.1: {} + + type-fest@3.13.1: {} + + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-byte-offset@1.0.2: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + + typedarray@0.0.6: {} + + typescript@5.5.4: {} + + uc.micro@2.1.0: {} + + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + + undici-types@6.19.8: {} + + unicode-canonical-property-names-ecmascript@2.0.0: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.1.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unified-args@11.0.1: + dependencies: + '@types/text-table': 0.2.5 + chalk: 5.3.0 + chokidar: 3.6.0 + comma-separated-tokens: 2.0.3 + json5: 2.2.3 + minimist: 1.2.8 + strip-ansi: 7.1.0 + text-table: 0.2.0 + unified-engine: 11.2.1 + transitivePeerDependencies: + - bluebird + - supports-color + + unified-engine@11.2.1: + dependencies: + '@types/concat-stream': 2.0.3 + '@types/debug': 4.1.12 + '@types/is-empty': 1.2.3 + '@types/node': 20.16.1 + '@types/unist': 3.0.3 + concat-stream: 2.0.0 + debug: 4.3.6(supports-color@8.1.1) + extend: 3.0.2 + glob: 10.4.5 + ignore: 5.3.2 + is-empty: 1.2.0 + is-plain-obj: 4.1.0 + load-plugin: 6.0.3 + parse-json: 7.1.1 + trough: 2.2.0 + unist-util-inspect: 8.1.0 + vfile: 6.0.2 + vfile-message: 4.0.2 + vfile-reporter: 8.1.1 + vfile-statistics: 3.0.0 + yaml: 2.5.0 + transitivePeerDependencies: + - bluebird + - supports-color + + unified-lint-rule@1.0.6: + dependencies: + wrapped: 1.0.1 + + unified-lint-rule@3.0.0: + dependencies: + '@types/unist': 3.0.3 + trough: 2.2.0 + unified: 11.0.5 + vfile: 6.0.2 + + unified-message-control@5.0.0: + dependencies: + '@types/unist': 3.0.3 + devlop: 1.1.0 + space-separated-tokens: 2.0.2 + unist-util-is: 6.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.2 + vfile-location: 5.0.3 + vfile-message: 4.0.2 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.2 + + unist-util-inspect@8.1.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-is@4.1.0: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@2.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@0.1.2: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + untildify@4.0.0: {} + + unzip-crx-3@0.2.0: + dependencies: + jszip: 3.10.1 + mkdirp: 0.5.6 + yaku: 0.16.7 + + update-browserslist-db@1.1.0(browserslist@4.23.3): + dependencies: + browserslist: 4.23.3 + escalade: 3.1.2 + picocolors: 1.0.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse-lax@3.0.0: + dependencies: + prepend-http: 2.0.0 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + utf8-byte-length@1.0.5: {} + + util-deprecate@1.0.2: {} + + uuid@8.3.2: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@5.0.1: {} + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + + verror@1.10.1: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + optional: true + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.2 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile-reporter@8.1.1: + dependencies: + '@types/supports-color': 8.1.3 + string-width: 6.1.0 + supports-color: 9.4.0 + unist-util-stringify-position: 4.0.0 + vfile: 6.0.2 + vfile-message: 4.0.2 + vfile-sort: 4.0.0 + vfile-statistics: 3.0.0 + + vfile-sort@4.0.0: + dependencies: + vfile: 6.0.2 + vfile-message: 4.0.2 + + vfile-statistics@3.0.0: + dependencies: + vfile: 6.0.2 + vfile-message: 4.0.2 + + vfile@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + + vite-node@2.0.5(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6): + dependencies: + cac: 6.7.14 + debug: 4.3.6(supports-color@8.1.1) + pathe: 1.1.2 + tinyrainbow: 1.2.0 + vite: 5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.41 + rollup: 4.21.0 + optionalDependencies: + '@types/node': 22.5.0 + fsevents: 2.3.3 + sass: 1.77.8 + terser: 5.31.6 + + vitest@2.0.5(@types/node@22.5.0)(jsdom@24.1.1)(sass@1.77.8)(terser@5.31.6): + dependencies: + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + debug: 4.3.6(supports-color@8.1.1) + execa: 8.0.1 + magic-string: 0.30.11 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.4.2(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6) + vite-node: 2.0.5(@types/node@22.5.0)(sass@1.77.8)(terser@5.31.6) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.5.0 + jsdom: 24.1.1 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vscode-uri@3.0.8: {} + + vue-component-type-helpers@2.0.29: {} + + vue-demi@0.14.10(vue@3.4.38(typescript@5.5.4)): + dependencies: + vue: 3.4.38(typescript@5.5.4) + + vue-eslint-parser@9.4.3(eslint@8.57.0): + dependencies: + debug: 4.3.6(supports-color@8.1.1) + eslint: 8.57.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + vue-tsc@2.0.29(typescript@5.5.4): + dependencies: + '@volar/typescript': 2.4.0 + '@vue/language-core': 2.0.29(typescript@5.5.4) + semver: 7.6.3 + typescript: 5.5.4 + + vue@3.4.38(typescript@5.5.4): + dependencies: + '@vue/compiler-dom': 3.4.38 + '@vue/compiler-sfc': 3.4.38 + '@vue/runtime-dom': 3.4.38 + '@vue/server-renderer': 3.4.38(vue@3.4.38(typescript@5.5.4)) + '@vue/shared': 3.4.38 + optionalDependencies: + typescript: 5.5.4 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + wait-on@7.2.0(debug@4.3.6): + dependencies: + axios: 1.7.5(debug@4.3.6) + joi: 17.13.3 + lodash: 4.17.21 + minimist: 1.2.8 + rxjs: 7.8.1 + transitivePeerDependencies: + - debug + + walk-up-path@3.0.1: {} + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.0.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + which-builtin-type@1.1.4: + dependencies: + function.prototype.name: 1.1.6 + has-tostringtag: 1.0.2 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.0.2 + is-generator-function: 1.0.10 + is-regex: 1.1.4 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.3 + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.1 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrapped@1.0.1: + dependencies: + co: 3.1.0 + sliced: 1.0.1 + + wrappy@1.0.2: {} + + ws@8.18.0: {} + + xml-name-validator@4.0.0: {} + + xml-name-validator@5.0.0: {} + + xmlbuilder@15.1.1: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yaku@0.16.7: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml-lint@1.7.0: + dependencies: + consola: 2.15.3 + globby: 11.1.0 + js-yaml: 4.1.0 + nconf: 0.12.1 + + yaml@2.5.0: {} + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + + zwitch@2.0.4: {} diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 00000000..6c786664 --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,9 @@ +const autoprefixer = require('autoprefixer'); + +module.exports = () => { + return { + plugins: [ + autoprefixer(), + ], + }; +}; diff --git a/scripts/configure_vscode.py b/scripts/configure_vscode.py new file mode 100644 index 00000000..29e83ab7 --- /dev/null +++ b/scripts/configure_vscode.py @@ -0,0 +1,190 @@ +""" +Description: + This script configures project-level VSCode settings in '.vscode/settings.json' for + development and installs recommended extensions from '.vscode/extensions.json'. + +Usage: + python3 ./scripts/configure_vscode.py +""" +# pylint: disable=missing-function-docstring + +import os +import json +from pathlib import Path +import subprocess +import sys +import re +from typing import Any, Optional +from shutil import which + +VSCODE_SETTINGS_JSON_FILE: str = '.vscode/settings.json' +VSCODE_EXTENSIONS_JSON_FILE: str = '.vscode/extensions.json' + +def main() -> None: + ensure_vscode_directory_exists() + ensure_setting_file_exists() + add_or_update_settings() + install_recommended_extensions() + +def ensure_vscode_directory_exists() -> None: + vscode_directory_path = os.path.dirname(VSCODE_SETTINGS_JSON_FILE) + try: + os.makedirs(vscode_directory_path, exist_ok=True) + print_success(f"Created or verified directory: {vscode_directory_path}") + except OSError as error: + print_error(f"Error handling directory {vscode_directory_path}: {error}") + +def ensure_setting_file_exists() -> None: + try: + if os.path.isfile(VSCODE_SETTINGS_JSON_FILE): + print_success(f"VSCode settings file exists: {VSCODE_SETTINGS_JSON_FILE}") + return + with open(VSCODE_SETTINGS_JSON_FILE, 'w', encoding='utf-8') as file: + json.dump({}, file, indent=4) + print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}") + except IOError as error: + print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}") + print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}") + +def add_or_update_settings() -> None: + configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript']) + # Set ESLint validation for specific file types. + # Details: # pylint: disable-next=line-too-long + # - https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code + + configure_setting_key('terminal.integrated.env.linux', {"GTK_PATH": ""}) + # Unset GTK_PATH on Linux for Electron development in sandboxed environments + # like Snap or Flatpak VSCode installations, enabling script execution. + # Details: # pylint: disable-next=line-too-long + # - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274 + + # Disable telemetry + configure_setting_key('redhat.telemetry.enabled', False) + configure_setting_key('gitlens.telemetry.enabled', False) + +def configure_setting_key(configuration_key: str, desired_value: Any) -> None: + try: + with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file: + settings: dict = json.load(file) + if configuration_key in settings: + actual_value = settings[configuration_key] + if actual_value == desired_value: + print_skip(f"Already configured as desired: \"{configuration_key}\"") + return + settings[configuration_key] = desired_value + file.seek(0) + json.dump(settings, file, indent=4) + file.truncate() + print_success(f"Added or updated configuration: {configuration_key}") + except json.JSONDecodeError: + print_error(f"Failed to update JSON for key {configuration_key}.") + +def install_recommended_extensions() -> None: + if not os.path.isfile(VSCODE_EXTENSIONS_JSON_FILE): + print_error( + f"The extensions.json file does not exist in the path: {VSCODE_EXTENSIONS_JSON_FILE}." + ) + return + with open(VSCODE_EXTENSIONS_JSON_FILE, 'r', encoding='utf-8') as file: + json_content: str = remove_json_comments(file.read()) + try: + data: dict = json.loads(json_content) + extensions: list[str] = data.get("recommendations", []) + if not extensions: + print_skip(f"No recommendations found in the {VSCODE_EXTENSIONS_JSON_FILE} file.") + return + vscode_cli_path = locate_vscode_cli() + if vscode_cli_path is None: + print_error('Visual Studio Code CLI (`code`) tool not found.') + return + install_vscode_extensions(vscode_cli_path, extensions) + except json.JSONDecodeError: + print_error(f"Invalid JSON in {VSCODE_EXTENSIONS_JSON_FILE}") + +def locate_vscode_cli() -> Optional[str]: + vscode_alias = which('code') # More reliable than using `code`, especially on Windows. + if vscode_alias: + return vscode_alias + potential_vscode_cli_paths = [ + # VS Code on macOS may not register 'code' command in PATH + '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code' + ] + for vscode_cli_candidate_path in potential_vscode_cli_paths: + if Path(vscode_cli_candidate_path).is_file(): + return vscode_cli_candidate_path + return None + +def remove_json_comments(json_like: str) -> str: + pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)' + return re.sub( + pattern, + lambda m: '' if m.group(1) else m.group(0), json_like, flags=re.MULTILINE, + ) + +def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None: + successful_installations = 0 + for ext in extensions: + try: + result = subprocess.run( + [vscode_cli_path, "--install-extension", ext], + check=True, + capture_output=True, + text=True, + ) + if "already installed" in result.stdout: + print_skip(f"Created or verified directory: {ext}") + else: + print_success(f"Installed extension: {ext}") + successful_installations += 1 + print_subprocess_output(result) + except subprocess.CalledProcessError as e: + print_subprocess_output(e) + print_error(f"Failed to install extension: {ext}") + except FileNotFoundError: + print_error(' '.join([ + f"Visual Studio Code CLI tool not found: {vscode_cli_path}." + f"Could not install extension: {ext}", + ])) + except Exception as e: # pylint: disable=broad-except + print_error(' '.join([ + f"Failed to install extension '{ext}'.", + f"Attempted using Visual Studio Code CLI at: '{vscode_cli_path}'.", + f"Encountered error: {e}", + ])) + total_extensions = len(extensions) + print_installation_results(successful_installations, total_extensions) + +def print_subprocess_output(result: subprocess.CompletedProcess[str]) -> None: + output = '\n'.join([text.strip() for text in [result.stdout, result.stderr] if text]) + if not output: + return + formatted_output = '\t' + output.strip().replace('\n', '\n\t') + print(formatted_output) + +def print_installation_results(successful_installations: int, total_extensions: int) -> None: + if successful_installations == total_extensions: + print_success( + f"Successfully installed or verified all {total_extensions} recommended extensions." + ) + elif successful_installations > 0: + print_warning( + f"Partially successful: Installed or verified {successful_installations} " + f"out of {total_extensions} recommended extensions." + ) + else: + print_error("Failed to install any of the recommended extensions.") + +def print_error(message: str) -> None: + print(f"[ERROR] {message}", file=sys.stderr) + +def print_success(message: str) -> None: + print(f"[SUCCESS] {message}") + +def print_skip(message: str) -> None: + print(f"[SKIPPED] {message}") + +def print_warning(message: str) -> None: + print(f"[WARNING] {message}", file=sys.stderr) + +if __name__ == "__main__": + main() diff --git a/scripts/logo-update.js b/scripts/logo-update.js new file mode 100644 index 00000000..191a6923 --- /dev/null +++ b/scripts/logo-update.js @@ -0,0 +1,234 @@ +/** + * Description: + * This script updates the logo images across the project based on the primary + * logo file ('img/logo.svg' file). + * + * It handles the creation and update of various icon sizes for different purposes, + * including desktop launcher icons, tray icons, and web favicons from a single source + * SVG logo file. + * + * Usage: + * node ./scripts/logo-update.js + * + * Notes: + * ImageMagick must be installed and accessible in the system's PATH + */ + +import { resolve, join, dirname } from 'node:path'; +import { stat } from 'node:fs/promises'; +import { spawn } from 'node:child_process'; +import { URL, fileURLToPath } from 'node:url'; +import electronBuilderConfig from '../electron-builder.cjs'; + +class ImageAssetPaths { + constructor(currentScriptDirectory) { + const projectRoot = resolve(currentScriptDirectory, '../'); + this.sourceImage = join(projectRoot, 'img/logo.svg'); + this.publicDirectory = join(projectRoot, 'src/presentation/public'); + this.electronBuildResourcesDirectory = electronBuilderConfig.directories.buildResources; + } + + get electronTrayIconFile() { + return join(this.publicDirectory, 'icon.png'); + } + + get webFaviconFile() { + return join(this.publicDirectory, 'favicon.ico'); + } + + toString() { + return `Source image: ${this.sourceImage}` + + `\nPublic directory: ${this.publicDirectory}` + + `\n\t Electron tray icon file: ${this.electronTrayIconFile}` + + `\n\t Web favicon file: ${this.webFaviconFile}` + + `\nElectron build directory: ${this.electronBuildResourcesDirectory}`; + } +} + +async function main() { + const paths = new ImageAssetPaths(getCurrentScriptDirectory()); + console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`); + const convertCommand = await findAvailableImageMagickCommand(); + await generateDesktopAndTrayIcons( + paths.sourceImage, + paths.electronTrayIconFile, + convertCommand, + ); + await generateWebFavicon( + paths.sourceImage, + paths.webFaviconFile, + convertCommand, + ); + await generateDesktopIcons( + paths.sourceImage, + paths.electronBuildResourcesDirectory, + convertCommand, + ); + console.log('🎉 (Re)created icons successfully.'); +} + +async function generateDesktopAndTrayIcons(sourceImage, targetFile, convertCommand) { + // Reference: https://web.archive.org/web/20240502124306/https://www.electronjs.org/docs/latest/api/tray + console.log(`Updating desktop launcher and tray icon at ${targetFile}.`); + await ensureFileExists(sourceImage); + await ensureParentFolderExists(targetFile); + await convertFromSvgToPng( + convertCommand, + sourceImage, + targetFile, + '512x512', + ); +} + +async function generateWebFavicon(sourceImage, faviconFilePath, convertCommand) { + console.log(`Updating favicon at ${faviconFilePath}.`); + await ensureFileExists(sourceImage); + await ensureParentFolderExists(faviconFilePath); + await convertFromSvgToIco( + convertCommand, + sourceImage, + faviconFilePath, + [16, 24, 32, 48, 64, 128, 256], + ); +} + +async function generateDesktopIcons(sourceImage, electronBuildResourcesDirectory, convertCommand) { + console.log(`Creating Electron icon files to ${electronBuildResourcesDirectory}.`); + // Reference: https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html + await ensureFolderExists(electronBuildResourcesDirectory); + await ensureFileExists(sourceImage); + const electronMainIconFile = join(electronBuildResourcesDirectory, 'icon.png'); + await convertFromSvgToPng( + convertCommand, + sourceImage, + electronMainIconFile, + '1024x1024', // Should be at least 512x512 + ); + // Relying on `electron-builder`s conversion from png to ico results in pixelated look on Windows + // 10 and 11 according to tests, see: + // - https://web.archive.org/web/20240502114650/https://github.com/electron-userland/electron-builder/issues/7328 + // - https://web.archive.org/web/20240502115448/https://github.com/electron-userland/electron-builder/issues/3867 + const electronWindowsIconFile = join(electronBuildResourcesDirectory, 'icon.ico'); + await convertFromSvgToIco( + convertCommand, + sourceImage, + electronWindowsIconFile, + [16, 24, 32, 48, 64, 128, 256], + ); +} + +async function ensureFileExists(filePath) { + const path = await stat(filePath); + if (!path.isFile()) { + throw new Error(`Not a file: ${filePath}`); + } +} + +async function ensureFolderExists(folderPath) { + if (!folderPath) { + throw new Error('Path is missing'); + } + const path = await stat(folderPath); + if (!path.isDirectory()) { + throw new Error(`Not a directory: ${folderPath}`); + } +} + +function ensureParentFolderExists(filePath) { + return ensureFolderExists(dirname(filePath)); +} + +const BaseImageMagickConvertArguments = Object.freeze([ + '-background none', // Transparent, so they do not get filled with white. + '-strip', // Strip metadata. + '-gravity Center', // Center the image when there's empty space +]); + +async function convertFromSvgToIco( + convertCommand, + inputFile, + outputFile, + sizes, +) { + await runCommand( + convertCommand, + ...BaseImageMagickConvertArguments, + `-density ${Math.max(...sizes).toString()}`, // High enough for sharpness + `-define icon:auto-resize=${sizes.map((s) => s.toString()).join(',')}`, // Automatically store multiple sizes in an ico image + '-compress None', + inputFile, + outputFile, + ); +} + +async function convertFromSvgToPng( + convertCommand, + inputFile, + outputFile, + size = undefined, +) { + await runCommand( + convertCommand, + ...BaseImageMagickConvertArguments, + ...(size === undefined ? [] : [ + `-resize ${size}`, + `-density ${size}`, // High enough for sharpness + ]), + inputFile, + outputFile, + ); +} + +async function runCommand(...args) { + const command = args.join(' '); + console.log(`Running command: ${command}`); + await new Promise((resolve, reject) => { + const process = spawn(command, { shell: true }); + process.stdout.on('data', (stdout) => { + console.log(stdout.toString()); + }); + process.stderr.on('data', (stderr) => { + console.error(stderr.toString()); + }); + process.on('error', (err) => { + reject(err); + }); + process.on('close', (exitCode) => { + if (exitCode !== 0) { + reject(new Error(`Process exited with non-zero exit code: ${exitCode}`)); + } else { + resolve(); + } + process.stdin.end(); + }); + }); +} + +function getCurrentScriptDirectory() { + return fileURLToPath(new URL('.', import.meta.url)); +} + +async function findAvailableImageMagickCommand() { + // Reference: https://web.archive.org/web/20240502120041/https://imagemagick.org/script/convert.php + const potentialBaseCommands = [ + 'convert', // Legacy command, usually available on Linux/macOS installations + 'magick convert', // Newer command, available on Windows installations + ]; + for (const baseCommand of potentialBaseCommands) { + const testCommand = `${baseCommand} -version`; + try { + await runCommand(testCommand); // eslint-disable-line no-await-in-loop + console.log(`Confirmed: ImageMagick command '${baseCommand}' is available and operational.`); + return baseCommand; + } catch (err) { + console.log(`Error: The command '${baseCommand}' is not found or failed to execute. Detailed error: ${err.message}"`); + } + } + throw new Error([ + 'Unable to locate any operational ImageMagick command.', + `Attempted commands were: ${potentialBaseCommands.join(', ')}.`, + 'Please ensure ImageMagick is correctly installed and accessible.', + ].join('\n')); +} + +await main(); diff --git a/scripts/npm-install.js b/scripts/npm-install.js new file mode 100644 index 00000000..2438822b --- /dev/null +++ b/scripts/npm-install.js @@ -0,0 +1,199 @@ +/* +Description: + This script manages NPM dependencies for a project. + It offers capabilities like doing a fresh install, retries on network errors, and other features. + +Usage: + npm run install-deps [-- ] + node scripts/npm-install.js [options] + +Options: + --root-directory + Specifies the root directory where package.json resides + Defaults to the current working directory. + Example: npm run install-deps -- --root-directory /your/path/here + + --no-errors + Ignores errors and continues the execution. + Example: npm run install-deps -- --no-errors + + --ci + Uses 'npm ci' for dependency installation instead of 'npm install'. + Example: npm run install-deps -- --ci + + --fresh + Removes the existing node_modules directory before installing dependencies. + Example: npm run install-deps -- --fresh + + --non-deterministic + Removes package-lock.json for a non-deterministic installation. + Example: npm run install-deps -- --non-deterministic + +Note: + + Flags can be combined as needed. + Example: npm run install-deps -- --fresh --non-deterministic +*/ + +import { exec } from 'node:child_process'; +import { resolve } from 'node:path'; +import { access, rm, unlink } from 'node:fs/promises'; +import { constants } from 'node:fs'; + +const MAX_RETRIES = 5; +const RETRY_DELAY_IN_MS = 5 /* seconds */ * 1000; +const ARG_NAMES = { + rootDirectory: '--root-directory', + ignoreErrors: '--no-errors', + ci: '--ci', + fresh: '--fresh', + nonDeterministic: '--non-deterministic', +}; + +async function main() { + const options = getOptions(); + console.log('Options:', options); + await ensureNpmRootDirectory(options.rootDirectory); + await ensureNpmIsAvailable(); + if (options.fresh) { + await removeNodeModules(options.rootDirectory); + } + if (options.nonDeterministic) { + await removePackageLockJson(options.rootDirectory); + } + const command = buildCommand(options.ci, options.outputErrors); + console.log('Starting dependency installation...'); + const exitCode = await executeWithRetry( + command, + options.workingDirectory, + MAX_RETRIES, + RETRY_DELAY_IN_MS, + ); + if (exitCode === 0) { + console.log('🎊 Installed dependencies...'); + } else { + console.error(`💀 Failed to install dependencies, exit code: ${exitCode}`); + } + process.exit(exitCode); +} + +async function removeNodeModules(workingDirectory) { + const nodeModulesDirectory = resolve(workingDirectory, 'node_modules'); + if (await exists('./node_modules')) { + console.log('Removing node_modules...'); + await rm(nodeModulesDirectory, { recursive: true }); + } +} + +async function removePackageLockJson(workingDirectory) { + const packageLockJsonFile = resolve(workingDirectory, 'package-lock.json'); + if (await exists(packageLockJsonFile)) { + console.log('Removing package-lock.json...'); + await unlink(packageLockJsonFile); + } +} + +async function ensureNpmIsAvailable() { + const exitCode = await executeCommand('npm --version'); + if (exitCode !== 0) { + throw new Error('`npm` in not available!'); + } +} + +async function ensureNpmRootDirectory(workingDirectory) { + const packageJsonPath = resolve(workingDirectory, 'package.json'); + if (!await exists(packageJsonPath)) { + throw new Error(`Not an NPM project root: ${workingDirectory}`); + } +} + +function buildCommand(ci, outputErrors) { + const baseCommand = ci ? 'npm ci' : 'npm install'; + if (!outputErrors) { + return `${baseCommand} --loglevel=error`; + } + return baseCommand; +} + +function getOptions() { + const processArgs = process.argv.slice(2); // Slice off the node and script name + return { + rootDirectory: processArgs.includes('--root-directory') ? processArgs[processArgs.indexOf('--root-directory') + 1] : process.cwd(), + outputErrors: !processArgs.includes(ARG_NAMES.ignoreErrors), + ci: processArgs.includes(ARG_NAMES.ci), + fresh: processArgs.includes(ARG_NAMES.fresh), + nonDeterministic: processArgs.includes(ARG_NAMES.nonDeterministic), + }; +} + +async function executeWithRetry( + command, + workingDirectory, + maxRetries, + retryDelayInMs, + currentAttempt = 1, +) { + const statusCode = await executeCommand(command, workingDirectory, true, true); + if (statusCode === 0 || currentAttempt >= maxRetries) { + return statusCode; + } + + console.log(`⚠️🔄 Attempt ${currentAttempt} failed. Retrying in ${retryDelayInMs / 1000} seconds...`); + await sleep(retryDelayInMs); + + const retryResult = await executeWithRetry( + command, + workingDirectory, + maxRetries, + retryDelayInMs, + currentAttempt + 1, + ); + return retryResult; +} + +async function executeCommand( + command, + workingDirectory = process.cwd(), + logStdout = false, + logCommand = false, +) { + if (logCommand) { + console.log(`▶️ Executing command "${command}" at "${workingDirectory}"`); + } + const process = exec( + command, + { + cwd: workingDirectory, + }, + ); + if (logStdout) { + process.stdout.on('data', (data) => { + console.log(data.toString()); + }); + } + process.stderr.on('data', (data) => { + console.error(data.toString()); + }); + return new Promise((resolve) => { + process.on('exit', (code) => { + resolve(code); + }); + }); +} + +function sleep(milliseconds) { + return new Promise((resolve) => { + setTimeout(resolve, milliseconds); + }); +} + +async function exists(path) { + try { + await access(path, constants.F_OK); + return true; + } catch { + return false; + } +} + +await main(); diff --git a/scripts/print-dist-dir.js b/scripts/print-dist-dir.js new file mode 100644 index 00000000..858204d9 --- /dev/null +++ b/scripts/print-dist-dir.js @@ -0,0 +1,58 @@ +/** + * Description: + * This script determines the absolute path of a distribution directory based on CLI arguments + * and outputs its absolute path. It is designed to be run programmatically by other scripts. + * + * Usage: + * node scripts/print-dist-dir.js [options] + * + * Options: + * --electron-unbundled Path for the unbundled Electron application + * --electron-bundled Path for the bundled Electron application + * --web Path for the web application + */ + +import { resolve } from 'node:path'; +import { readFile } from 'node:fs/promises'; + +const DIST_DIRS_JSON_FILE_PATH = resolve(process.cwd(), 'dist-dirs.json'); // cannot statically import because ESLint does not support it https://github.com/eslint/eslint/discussions/15305 +const CLI_ARGUMENTS = process.argv.slice(2); + +async function main() { + const distDirs = await readDistDirsJsonFile(DIST_DIRS_JSON_FILE_PATH); + const relativeDistDir = determineRelativeDistDir(distDirs, CLI_ARGUMENTS); + const absoluteDistDir = resolve(process.cwd(), relativeDistDir); + console.log(absoluteDistDir); +} + +function mapCliFlagsToDistDirs(distDirs) { + return { + '--electron-unbundled': distDirs.electronUnbundled, + '--electron-bundled': distDirs.electronBundled, + '--web': distDirs.web, + }; +} + +function determineRelativeDistDir(distDirsJsonObject, cliArguments) { + const cliFlagDistDirMap = mapCliFlagsToDistDirs(distDirsJsonObject); + const availableCliFlags = Object.keys(cliFlagDistDirMap); + const requestedCliFlags = cliArguments.filter((arg) => { + return availableCliFlags.includes(arg); + }); + if (!requestedCliFlags.length) { + throw new Error(`No distribution directory was requested. Please use one of these flags: ${availableCliFlags.join(', ')}`); + } + if (requestedCliFlags.length > 1) { + throw new Error(`Multiple distribution directories were requested, but this script only supports one: ${requestedCliFlags.join(', ')}`); + } + const selectedCliFlag = requestedCliFlags[0]; + return cliFlagDistDirMap[selectedCliFlag]; +} + +async function readDistDirsJsonFile(absoluteConfigJsonFilePath) { + const fileContentAsText = await readFile(absoluteConfigJsonFilePath, 'utf8'); + const parsedJsonData = JSON.parse(fileContentAsText); + return parsedJsonData; +} + +await main(); diff --git a/scripts/validate-collections-yaml/README.md b/scripts/validate-collections-yaml/README.md new file mode 100644 index 00000000..c9553ca4 --- /dev/null +++ b/scripts/validate-collections-yaml/README.md @@ -0,0 +1,51 @@ +# validate-collections-yaml + +This script validates YAML collection files against a predefined schema to ensure their integrity. + +## Prerequisites + +- Python 3.x installed on your system. + +## Running in a Virtual Environment (Recommended) + +Using a virtual environment isolates dependencies and prevents conflicts. + +1. **Create a virtual environment:** + + ```bash + python3 -m venv ./scripts/validate-collections-yaml/.venv + ``` + +2. **Activate the virtual environment:** + + ```bash + source ./scripts/validate-collections-yaml/.venv/bin/activate + ``` + +3. **Install dependencies:** + + ```bash + python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt + ``` + +4. **Run the script:** + + ```bash + python3 ./scripts/validate-collections-yaml + ``` + +## Running Globally + +Running the script globally is less recommended due to potential dependency conflicts. + +1. **Install dependencies:** + + ```bash + python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt + ``` + +2. **Run the script:** + + ```bash + python3 ./scripts/validate-collections-yaml + ``` diff --git a/scripts/validate-collections-yaml/__main__.py b/scripts/validate-collections-yaml/__main__.py new file mode 100644 index 00000000..150e7ec3 --- /dev/null +++ b/scripts/validate-collections-yaml/__main__.py @@ -0,0 +1,62 @@ +""" +Description: + This script validates collection YAML files against the expected schema. + +Usage: + python3 ./scripts/validate-collections-yaml + +Notes: + This script requires the `jsonschema` and `pyyaml` packages (see requirements.txt). +""" +# pylint: disable=missing-function-docstring +from os import path +import sys +from glob import glob +from typing import List +from jsonschema import exceptions, validate # pylint: disable=import-error +import yaml # pylint: disable=import-error + +SCHEMA_FILE_PATH = './src/application/collections/.schema.yaml' +COLLECTIONS_GLOB_PATTERN = './src/application/collections/*.yaml' + +def main() -> None: + schema_yaml = read_file(SCHEMA_FILE_PATH) + schema_json = convert_yaml_to_json(schema_yaml) + collection_file_paths = find_collection_files(COLLECTIONS_GLOB_PATTERN) + print(f'Found {len(collection_file_paths)} YAML files to validate.') + + total_invalid_files = 0 + for collection_file_path in collection_file_paths: + file_name = path.basename(collection_file_path) + print(f'Validating {file_name}...') + collection_yaml = read_file(collection_file_path) + collection_json = convert_yaml_to_json(collection_yaml) + try: + validate(instance=collection_json, schema=schema_json) + print(f'Success: {file_name} is valid.') + except exceptions.ValidationError as err: + print(f'Error: Validation failed for {file_name}.', file=sys.stderr) + print(str(err), file=sys.stderr) + total_invalid_files += 1 + + if total_invalid_files > 0: + print(f'Validation complete with {total_invalid_files} invalid files.', file=sys.stderr) + sys.exit(1) + else: + print('Validation complete. All files are valid.') + sys.exit(0) + +def read_file(file_path: str) -> str: + with open(file_path, 'r', encoding='utf-8') as file: + return file.read() + +def find_collection_files(glob_pattern: str) -> List[str]: + files = glob(glob_pattern) + filtered_files = [f for f in files if not path.basename(f).startswith('.')] + return filtered_files + +def convert_yaml_to_json(yaml_content: str) -> dict: + return yaml.safe_load(yaml_content) + +if __name__ == '__main__': + main() diff --git a/scripts/validate-collections-yaml/requirements.txt b/scripts/validate-collections-yaml/requirements.txt new file mode 100644 index 00000000..a6d6f3ad --- /dev/null +++ b/scripts/validate-collections-yaml/requirements.txt @@ -0,0 +1,6 @@ +attrs==23.2.0 +jsonschema==4.22.0 +jsonschema-specifications==2023.12.1 +PyYAML==6.0.1 +referencing==0.35.1 +rpds-py==0.18.1 diff --git a/scripts/verify-build-artifacts.js b/scripts/verify-build-artifacts.js new file mode 100644 index 00000000..b29354aa --- /dev/null +++ b/scripts/verify-build-artifacts.js @@ -0,0 +1,133 @@ +/** + * Description: + * This script verifies the existence and content of build artifacts based on the + * provided CLI flags. It exists with exit code `0` if all verifications pass, otherwise + * with exit code `1`. + * + * Usage: + * node scripts/verify-build-artifacts.js [options] + * + * Options: + * --electron-unbundled Verify artifacts for the unbundled Electron application. + * --electron-bundled Verify artifacts for the bundled Electron application. + * --web Verify artifacts for the web application. + */ + +import { access, readdir } from 'node:fs/promises'; +import { exec } from 'node:child_process'; +import { resolve } from 'node:path'; + +const PROCESS_ARGUMENTS = process.argv.slice(2); +const PRINT_DIST_DIR_SCRIPT_BASE_COMMAND = 'node scripts/print-dist-dir'; + +async function main() { + const buildConfigs = getBuildVerificationConfigs(); + if (!anyCommandsFound(Object.keys(buildConfigs))) { + die(`No valid command found in process arguments. Expected one of: ${Object.keys(buildConfigs).join(', ')}`); + } + /* eslint-disable no-await-in-loop */ + for (const [command, config] of Object.entries(buildConfigs)) { + if (PROCESS_ARGUMENTS.includes(command)) { + const distDir = await executePrintDistDirScript(config.printDistDirScriptArgument); + await verifyDirectoryExists(distDir); + await verifyNonEmptyDirectory(distDir); + await verifyFilesExist(distDir, config.filePatterns); + } + } + /* eslint-enable no-await-in-loop */ + console.log('✅ Build completed successfully and all expected artifacts are in place.'); + process.exit(0); +} + +function getBuildVerificationConfigs() { + return { + '--electron-unbundled': { + printDistDirScriptArgument: '--electron-unbundled', + filePatterns: [ + /main[/\\]index\.(cjs|mjs|js)/, + /preload[/\\]index\.(cjs|mjs|js)/, + /renderer[/\\]index\.htm(l)?/, + ], + }, + '--electron-bundled': { + printDistDirScriptArgument: '--electron-bundled', + filePatterns: [ + /latest.*\.yml/, // generates latest.yml for auto-updates + /.*-\d+\.\d+\.\d+\..*/, // a file with extension and semantic version (packaged application) + ], + }, + '--web': { + printDistDirScriptArgument: '--web', + filePatterns: [ + /index\.htm(l)?/, + ], + }, + }; +} + +function anyCommandsFound(commands) { + return PROCESS_ARGUMENTS.some((arg) => commands.includes(arg)); +} + +async function verifyDirectoryExists(directoryPath) { + try { + await access(directoryPath); + } catch (error) { + die(`Directory does not exist at \`${directoryPath}\`:\n\t${error.message}`); + } +} + +async function verifyNonEmptyDirectory(directoryPath) { + const files = await readdir(directoryPath); + if (files.length === 0) { + die(`Directory is empty at \`${directoryPath}\``); + } +} + +async function verifyFilesExist(directoryPath, filePatterns) { + const files = await listAllFilesRecursively(directoryPath); + for (const pattern of filePatterns) { + const match = files.some((file) => pattern.test(file)); + if (!match) { + die( + `No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``, + `\nFiles in directory:\n${files.map((file) => `- ${file}`).join('\n')}`, + ); + } + } +} + +async function listAllFilesRecursively(directoryPath) { + const dir = await readdir(directoryPath, { withFileTypes: true }); + const files = await Promise.all(dir.map(async (dirent) => { + const absolutePath = resolve(directoryPath, dirent.name); + if (dirent.isDirectory()) { + return listAllFilesRecursively(absolutePath); + } + return absolutePath; + })); + return files.flat(); +} + +async function executePrintDistDirScript(flag) { + return new Promise((resolve, reject) => { + const commandToRun = `${PRINT_DIST_DIR_SCRIPT_BASE_COMMAND} ${flag}`; + + exec(commandToRun, (error, stdout, stderr) => { + if (error) { + reject(new Error(`Execution failed with error: ${error}`)); + } else if (stderr) { + reject(new Error(`Execution failed with stderr: ${stderr}`)); + } else { + resolve(stdout.trim()); + } + }); + }); +} + +function die(...message) { + console.error(...message); + process.exit(1); +} + +await main(); diff --git a/scripts/verify-web-server-status.js b/scripts/verify-web-server-status.js new file mode 100644 index 00000000..f1ad10fb --- /dev/null +++ b/scripts/verify-web-server-status.js @@ -0,0 +1,87 @@ +#!/usr/bin/env node + +/** + * Description: + * This script checks if a server, provided as a CLI argument, is up + * and returns an HTTP 200 status code. + * It is designed to provide easy verification of server availability + * and will retry a specified number of times. + * + * Usage: + * node ./scripts/verify-web-server-status.js --url [URL] [--max-retries NUMBER] + * + * Options: + * --url URL of the server to check + * --max-retries Maximum number of retry attempts (default: 30) + */ + +const DEFAULT_MAX_RETRIES = 30; +const RETRY_DELAY_IN_SECONDS = 3; +const PARAMETER_NAME_URL = '--url'; +const PARAMETER_NAME_MAX_RETRIES = '--max-retries'; + +async function checkServer(currentRetryCount = 1) { + const serverUrl = readRequiredParameterValue(PARAMETER_NAME_URL); + const maxRetries = parseNumber( + readOptionalParameterValue(PARAMETER_NAME_MAX_RETRIES, DEFAULT_MAX_RETRIES), + ); + console.log(`🌐 Requesting ${serverUrl}...`); + try { + const response = await fetch(serverUrl); + if (response.status === 200) { + console.log('🎊 Success: The server is up and returned HTTP 200.'); + process.exit(0); + } else { + exitWithError(`Server returned unexpected HTTP status code ${response.statusCode}.`); + } + } catch (error) { + console.error('Error making the request:', error); + scheduleNextRetry(maxRetries, currentRetryCount); + } +} + +function scheduleNextRetry(maxRetries, currentRetryCount) { + console.log(`Attempt ${currentRetryCount}/${maxRetries}:`); + console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`); + + const remainingTime = (maxRetries - currentRetryCount) * RETRY_DELAY_IN_SECONDS; + console.log(`Time remaining before timeout: ${remainingTime}s`); + + if (currentRetryCount < maxRetries) { + setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000); + } else { + exitWithError('The server at did not return HTTP 200 within the allocated time.'); + } +} + +function readRequiredParameterValue(parameterName) { + const parameterValue = readOptionalParameterValue(parameterName); + if (parameterValue === undefined) { + exitWithError(`Parameter "${parameterName}" is required but not provided.`); + } + return parameterValue; +} + +function readOptionalParameterValue(parameterName, defaultValue) { + const index = process.argv.indexOf(parameterName); + if (index === -1 || index === process.argv.length - 1) { + return defaultValue; + } + return process.argv[index + 1]; +} + +function parseNumber(numberLike) { + const number = parseInt(numberLike, 10); + if (Number.isNaN(number)) { + exitWithError(`Invalid number: ${numberLike}`); + } + return number; +} + +function exitWithError(message) { + console.error(`Failure: ${message}`); + console.log('Exiting'); + process.exit(1); +} + +await checkServer(); diff --git a/src/TypeHelpers.ts b/src/TypeHelpers.ts new file mode 100644 index 00000000..c41522a1 --- /dev/null +++ b/src/TypeHelpers.ts @@ -0,0 +1,48 @@ +export type Constructible = { + prototype: T; + apply: (this: unknown, args: TArgs) => void; + readonly name: string; +}; + +export type PropertyKeys = { + [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : K; +}[keyof T]; + +export type ConstructorArguments = + T extends new (...args: infer U) => unknown ? U : never; + +export type FunctionKeys = { + [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never; +}[keyof T]; + +export function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +export function isNumber(value: unknown): value is number { + return typeof value === 'number'; +} + +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; +} + +export function isFunction(value: unknown): value is (...args: unknown[]) => unknown { + return typeof value === 'function'; +} + +export function isArray(value: unknown): value is Array { + return Array.isArray(value); +} + +export function isPlainObject( + variable: unknown, +): variable is object & Record { + return Boolean(variable) // the data type of null is an object + && typeof variable === 'object' + && !Array.isArray(variable); +} + +export function isNullOrUndefined(value: unknown): value is (null | undefined) { + return typeof value === 'undefined' || value === null; +} diff --git a/src/application/ApplicationFactory.ts b/src/application/ApplicationFactory.ts new file mode 100644 index 00000000..9c851a52 --- /dev/null +++ b/src/application/ApplicationFactory.ts @@ -0,0 +1,21 @@ +import type { IApplication } from '@/domain/IApplication'; +import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; +import { parseApplication } from './Parser/ApplicationParser'; +import type { IApplicationFactory } from './IApplicationFactory'; + +export type ApplicationGetterType = () => IApplication; +const ApplicationGetter: ApplicationGetterType = parseApplication; + +export class ApplicationFactory implements IApplicationFactory { + public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter); + + private readonly getter: AsyncLazy; + + protected constructor(costlyGetter: ApplicationGetterType) { + this.getter = new AsyncLazy(() => Promise.resolve(costlyGetter())); + } + + public getApp(): Promise { + return this.getter.getValue(); + } +} diff --git a/src/application/CodeRunner/CodeRunner.ts b/src/application/CodeRunner/CodeRunner.ts new file mode 100644 index 00000000..b8d7a479 --- /dev/null +++ b/src/application/CodeRunner/CodeRunner.ts @@ -0,0 +1,38 @@ +export interface CodeRunner { + runCode( + code: string, + fileExtension: string, + ): Promise; +} + +export type CodeRunOutcome = SuccessfulCodeRun | FailedCodeRun; + +export type CodeRunErrorType = + | 'FileWriteError' + | 'FileReadbackVerificationError' + | 'FilePathGenerationError' + | 'UnsupportedPlatform' + | 'DirectoryCreationError' + | 'FilePermissionChangeError' + | 'FileExecutionError' + | 'ExternalProcessTermination'; + +interface CodeRunStatus { + readonly success: boolean; + readonly error?: CodeRunError; +} + +interface SuccessfulCodeRun extends CodeRunStatus { + readonly success: true; + readonly error?: undefined; +} + +export interface FailedCodeRun extends CodeRunStatus { + readonly success: false; + readonly error: CodeRunError; +} + +export interface CodeRunError { + readonly type: CodeRunErrorType; + readonly message: string; +} diff --git a/src/application/CodeRunner/ScriptFilename.ts b/src/application/CodeRunner/ScriptFilename.ts new file mode 100644 index 00000000..2e56d983 --- /dev/null +++ b/src/application/CodeRunner/ScriptFilename.ts @@ -0,0 +1 @@ +export const ScriptFilename = 'privacy-script' as const; diff --git a/src/application/Common/Array.ts b/src/application/Common/Array.ts new file mode 100644 index 00000000..c48b831a --- /dev/null +++ b/src/application/Common/Array.ts @@ -0,0 +1,17 @@ +// Compares to Array objects for equality, ignoring order +export function scrambledEqual(array1: readonly T[], array2: readonly T[]) { + const sortedArray1 = sort(array1); + const sortedArray2 = sort(array2); + return sequenceEqual(sortedArray1, sortedArray2); + function sort(array: readonly T[]) { + return array.slice().sort(); + } +} + +// Compares to Array objects for equality in same order +export function sequenceEqual(array1: readonly T[], array2: readonly T[]) { + if (array1.length !== array2.length) { + return false; + } + return array1.every((val, index) => val === array2[index]); +} diff --git a/src/application/Common/CustomError.ts b/src/application/Common/CustomError.ts new file mode 100644 index 00000000..eff52bf6 --- /dev/null +++ b/src/application/Common/CustomError.ts @@ -0,0 +1,54 @@ +import { isFunction, type ConstructorArguments } from '@/TypeHelpers'; + +/* + Provides a unified and resilient way to extend errors across platforms. + + Rationale: + - Babel: + > "Built-in classes cannot be properly subclassed due to limitations in ES5" + > https://web.archive.org/web/20230810014108/https://babeljs.io/docs/caveats#classes + - TypeScript: + > "Extending built-ins like Error, Array, and Map may no longer work" + > https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work +*/ +export abstract class CustomError extends Error { + constructor(...args: ConstructorArguments) { + super(...args); + + fixPrototype(this, new.target.prototype); + ensureStackTrace(this); + + this.name = this.constructor.name; + } +} + +interface ErrorPrototypeManipulation { + getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined); + getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined); +} + +export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = { + getSetPrototypeOf: () => Object.setPrototypeOf, + getCaptureStackTrace: () => Error.captureStackTrace, +}; + +function fixPrototype(target: Error, prototype: CustomError) { + // This is recommended by TypeScript guidelines. + // Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + // Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget + const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf(); + if (!isFunction(setPrototypeOf)) { + return; + } + setPrototypeOf(target, prototype); +} + +function ensureStackTrace(target: Error) { + const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace(); + if (!isFunction(captureStackTrace)) { + // captureStackTrace is only available on V8, if it's not available + // modern JS engines will usually generate a stack trace on error objects when they're thrown. + return; + } + captureStackTrace(target, target.constructor); +} diff --git a/src/application/Common/Enum.ts b/src/application/Common/Enum.ts new file mode 100644 index 00000000..81b9384a --- /dev/null +++ b/src/application/Common/Enum.ts @@ -0,0 +1,62 @@ +import { isString } from '@/TypeHelpers'; + +// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611 +export type EnumType = number | string; +export type EnumVariable + = { [key in T]: TEnumValue }; + +export interface EnumParser { + parseEnum(value: string, propertyName: string): TEnum; +} + +export function createEnumParser( + enumVariable: EnumVariable, +): EnumParser { + return { + parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), + }; +} + +function parseEnumValue( + value: string, + enumName: string, + enumVariable: EnumVariable, +): TEnumValue { + if (!value) { + throw new Error(`missing ${enumName}`); + } + if (!isString(value)) { + throw new Error(`unexpected type of ${enumName}: "${typeof value}"`); + } + const casedValue = getEnumNames(enumVariable) + .find((enumValue) => enumValue.toLowerCase() === value.toLowerCase()); + if (!casedValue) { + throw new Error(`unknown ${enumName}: "${value}"`); + } + return enumVariable[casedValue as keyof typeof enumVariable]; +} + +export function getEnumNames +( + enumVariable: EnumVariable, +): string[] { + return Object + .values(enumVariable) + .filter((enumMember): enumMember is string => isString(enumMember)); +} + +export function getEnumValues( + enumVariable: EnumVariable, +): TEnumValue[] { + return getEnumNames(enumVariable) + .map((level) => enumVariable[level]) as TEnumValue[]; +} + +export function assertInRange( + value: TEnumValue, + enumVariable: EnumVariable, +) { + if (!(value in enumVariable)) { + throw new RangeError(`enum value "${value}" is out of range`); + } +} diff --git a/src/application/Common/Log/Logger.ts b/src/application/Common/Log/Logger.ts new file mode 100644 index 00000000..39ed5169 --- /dev/null +++ b/src/application/Common/Log/Logger.ts @@ -0,0 +1,6 @@ +export interface Logger { + info(...params: unknown[]): void; + warn(...params: unknown[]): void; + error(...params: unknown[]): void; + debug(...params: unknown[]): void; +} diff --git a/src/application/Common/ScriptingLanguage/IScriptingLanguageFactory.ts b/src/application/Common/ScriptingLanguage/IScriptingLanguageFactory.ts new file mode 100644 index 00000000..fa18479e --- /dev/null +++ b/src/application/Common/ScriptingLanguage/IScriptingLanguageFactory.ts @@ -0,0 +1,5 @@ +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; + +export interface IScriptingLanguageFactory { + create(language: ScriptingLanguage): T; +} diff --git a/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts b/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts new file mode 100644 index 00000000..c39be6a1 --- /dev/null +++ b/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts @@ -0,0 +1,27 @@ +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import { assertInRange } from '@/application/Common/Enum'; +import type { IScriptingLanguageFactory } from './IScriptingLanguageFactory'; + +type Getter = () => T; + +export abstract class ScriptingLanguageFactory implements IScriptingLanguageFactory { + private readonly getters = new Map>(); + + public create(language: ScriptingLanguage): T { + assertInRange(language, ScriptingLanguage); + const getter = this.getters.get(language); + if (!getter) { + throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`); + } + const instance = getter(); + return instance; + } + + protected registerGetter(language: ScriptingLanguage, getter: Getter) { + assertInRange(language, ScriptingLanguage); + if (this.getters.has(language)) { + throw new Error(`${ScriptingLanguage[language]} is already registered`); + } + this.getters.set(language, getter); + } +} diff --git a/src/application/Common/Shuffle.ts b/src/application/Common/Shuffle.ts new file mode 100644 index 00000000..94df09bd --- /dev/null +++ b/src/application/Common/Shuffle.ts @@ -0,0 +1,12 @@ +/* + Shuffle an array of strings, returning a new array with elements in random order. + Uses the Fisher-Yates (or Durstenfeld) algorithm. +*/ +export function shuffle(array: readonly T[]): T[] { + const shuffledArray = [...array]; + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; + } + return shuffledArray; +} diff --git a/src/application/Common/Text/FilterEmptyStrings.ts b/src/application/Common/Text/FilterEmptyStrings.ts new file mode 100644 index 00000000..aca4e2a9 --- /dev/null +++ b/src/application/Common/Text/FilterEmptyStrings.ts @@ -0,0 +1,25 @@ +import { isArray } from '@/TypeHelpers'; + +export type OptionalString = string | undefined | null; + +export function filterEmptyStrings( + texts: readonly OptionalString[], + isArrayType: typeof isArray = isArray, +): string[] { + if (!isArrayType(texts)) { + throw new Error(`Invalid input: Expected an array, but received type ${typeof texts}.`); + } + assertArrayItemsAreStringLike(texts); + return texts + .filter((title): title is string => Boolean(title)); +} + +function assertArrayItemsAreStringLike( + texts: readonly unknown[], +): asserts texts is readonly OptionalString[] { + const invalidItems = texts.filter((item) => !(typeof item === 'string' || item === undefined || item === null)); + if (invalidItems.length > 0) { + const invalidTypes = invalidItems.map((item) => typeof item).join(', '); + throw new Error(`Invalid array items: Expected items as string, undefined, or null. Received invalid types: ${invalidTypes}.`); + } +} diff --git a/src/application/Common/Text/IndentText.ts b/src/application/Common/Text/IndentText.ts new file mode 100644 index 00000000..ed7d104a --- /dev/null +++ b/src/application/Common/Text/IndentText.ts @@ -0,0 +1,29 @@ +import { isString } from '@/TypeHelpers'; +import { splitTextIntoLines } from './SplitTextIntoLines'; + +export function indentText( + text: string, + indentLevel = 1, + utilities: TextIndentationUtilities = DefaultUtilities, +): string { + if (!utilities.isStringType(text)) { + throw new Error(`Indentation error: The input must be a string. Received type: ${typeof text}.`); + } + if (indentLevel <= 0) { + throw new Error(`Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`); + } + const indentation = '\t'.repeat(indentLevel); + return utilities.splitIntoLines(text) + .map((line) => (line ? `${indentation}${line}` : line)) + .join('\n'); +} + +interface TextIndentationUtilities { + readonly splitIntoLines: typeof splitTextIntoLines; + readonly isStringType: typeof isString; +} + +const DefaultUtilities: TextIndentationUtilities = { + splitIntoLines: splitTextIntoLines, + isStringType: isString, +}; diff --git a/src/application/Common/Text/SplitTextIntoLines.ts b/src/application/Common/Text/SplitTextIntoLines.ts new file mode 100644 index 00000000..a57871c9 --- /dev/null +++ b/src/application/Common/Text/SplitTextIntoLines.ts @@ -0,0 +1,11 @@ +import { isString } from '@/TypeHelpers'; + +export function splitTextIntoLines( + text: string, + isStringType = isString, +): string[] { + if (!isStringType(text)) { + throw new Error(`Line splitting error: Expected a string but received type '${typeof text}'.`); + } + return text.split(/\r\n|\r|\n/); +} diff --git a/src/application/Common/Timing/BatchedDebounce.ts b/src/application/Common/Timing/BatchedDebounce.ts new file mode 100644 index 00000000..0484e801 --- /dev/null +++ b/src/application/Common/Timing/BatchedDebounce.ts @@ -0,0 +1,27 @@ +import { PlatformTimer } from './PlatformTimer'; +import type { TimeoutType, Timer } from './Timer'; + +export function batchedDebounce( + callback: (batches: readonly T[]) => void, + waitInMs: number, + timer: Timer = PlatformTimer, +): (arg: T) => void { + let lastTimeoutId: TimeoutType | undefined; + let batches: Array = []; + + return (arg: T) => { + batches.push(arg); + + const later = () => { + callback(batches); + batches = []; + lastTimeoutId = undefined; + }; + + if (lastTimeoutId !== undefined) { + timer.clearTimeout(lastTimeoutId); + } + + lastTimeoutId = timer.setTimeout(later, waitInMs); + }; +} diff --git a/src/application/Common/Timing/PlatformTimer.ts b/src/application/Common/Timing/PlatformTimer.ts new file mode 100644 index 00000000..e016c526 --- /dev/null +++ b/src/application/Common/Timing/PlatformTimer.ts @@ -0,0 +1,7 @@ +import type { Timer } from './Timer'; + +export const PlatformTimer: Timer = { + setTimeout: (callback, ms) => setTimeout(callback, ms), + clearTimeout: (timeoutId) => clearTimeout(timeoutId), + dateNow: () => Date.now(), +}; diff --git a/src/application/Common/Timing/Throttle.ts b/src/application/Common/Timing/Throttle.ts new file mode 100644 index 00000000..fa95d087 --- /dev/null +++ b/src/application/Common/Timing/Throttle.ts @@ -0,0 +1,164 @@ +/* eslint-disable max-classes-per-file */ +import { PlatformTimer } from './PlatformTimer'; +import type { Timer, TimeoutType } from './Timer'; + +export type CallbackType = (..._: readonly unknown[]) => void; + +export interface ThrottleOptions { + /** Skip the immediate execution of the callback on the first invoke */ + readonly excludeLeadingCall: boolean; + readonly timer: Timer; +} + +const DefaultOptions: ThrottleOptions = { + excludeLeadingCall: false, + timer: PlatformTimer, +}; + +export interface ThrottleFunction { + ( + callback: CallbackType, + waitInMs: number, + options?: Partial, + ): CallbackType; +} + +export const throttle: ThrottleFunction = ( + callback: CallbackType, + waitInMs: number, + options: Partial = DefaultOptions, +): CallbackType => { + const defaultedOptions: ThrottleOptions = { + ...DefaultOptions, + ...options, + }; + const throttler = new Throttler(waitInMs, callback, defaultedOptions); + return (...args: unknown[]) => throttler.invoke(...args); +}; + +class Throttler { + private lastExecutionTime: number | null = null; + + private executionScheduler: DelayedCallbackScheduler; + + constructor( + private readonly waitInMs: number, + private readonly callback: CallbackType, + private readonly options: ThrottleOptions, + ) { + if (!waitInMs) { throw new Error('missing delay'); } + if (waitInMs < 0) { throw new Error('negative delay'); } + this.executionScheduler = new DelayedCallbackScheduler(options.timer); + } + + public invoke(...args: unknown[]): void { + switch (true) { + case this.isLeadingCallWithinThrottlePeriod(): { + if (this.options.excludeLeadingCall) { + this.scheduleNext(args); + return; + } + this.executeNow(args); + return; + } + case this.isAlreadyScheduled(): { + this.updateNextScheduled(args); + return; + } + case !this.isThrottlePeriodPassed(): { + this.scheduleNext(args); + return; + } + default: + throw new Error('Throttle logical error: no conditions for execution or scheduling were met.'); + } + } + + private isLeadingCallWithinThrottlePeriod(): boolean { + return this.isThrottlePeriodPassed() + && !this.isAlreadyScheduled(); + } + + private isThrottlePeriodPassed(): boolean { + if (this.lastExecutionTime === null) { + return true; + } + const timeSinceLastExecution = this.options.timer.dateNow() - this.lastExecutionTime; + const isThrottleTimePassed = timeSinceLastExecution >= this.waitInMs; + return isThrottleTimePassed; + } + + private isAlreadyScheduled(): boolean { + return this.executionScheduler.getNext() !== null; + } + + private scheduleNext(args: unknown[]): void { + if (this.executionScheduler.getNext()) { + throw new Error('An execution is already scheduled.'); + } + this.executionScheduler.resetNext( + () => this.executeNow(args), + this.waitInMs, + ); + } + + private updateNextScheduled(args: unknown[]): void { + const nextScheduled = this.executionScheduler.getNext(); + if (!nextScheduled) { + throw new Error('A non-existent scheduled execution cannot be updated.'); + } + const nextDelay = nextScheduled.scheduledTime - this.dateNow(); + this.executionScheduler.resetNext( + () => this.executeNow(args), + nextDelay, + ); + } + + private executeNow(args: unknown[]): void { + this.callback(...args); + this.lastExecutionTime = this.dateNow(); + } + + private dateNow(): number { + return this.options.timer.dateNow(); + } +} + +interface ScheduledCallback { + readonly scheduleTimeoutId: TimeoutType; + readonly scheduledTime: number; +} + +class DelayedCallbackScheduler { + private scheduledCallback: ScheduledCallback | null = null; + + constructor( + private readonly timer: Timer, + ) { } + + public getNext(): ScheduledCallback | null { + return this.scheduledCallback; + } + + public resetNext( + callback: () => void, + delayInMs: number, + ) { + this.clear(); + this.scheduledCallback = { + scheduledTime: this.timer.dateNow() + delayInMs, + scheduleTimeoutId: this.timer.setTimeout(() => { + this.clear(); + callback(); + }, delayInMs), + }; + } + + private clear() { + if (this.scheduledCallback === null) { + return; + } + this.timer.clearTimeout(this.scheduledCallback.scheduleTimeoutId); + this.scheduledCallback = null; + } +} diff --git a/src/application/Common/Timing/Timer.ts b/src/application/Common/Timing/Timer.ts new file mode 100644 index 00000000..fe23b612 --- /dev/null +++ b/src/application/Common/Timing/Timer.ts @@ -0,0 +1,8 @@ +// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number) +export type TimeoutType = ReturnType; + +export interface Timer { + setTimeout: (callback: () => void, ms: number) => TimeoutType; + clearTimeout: (timeoutId: TimeoutType) => void; + dateNow(): number; +} diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts new file mode 100644 index 00000000..4828947a --- /dev/null +++ b/src/application/Context/ApplicationContext.ts @@ -0,0 +1,55 @@ +import type { IApplication } from '@/domain/IApplication'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import { EventSource } from '@/infrastructure/Events/EventSource'; +import { assertInRange } from '@/application/Common/Enum'; +import { CategoryCollectionState } from './State/CategoryCollectionState'; +import type { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext'; +import type { ICategoryCollectionState } from './State/ICategoryCollectionState'; + +type StateMachine = Map; + +export class ApplicationContext implements IApplicationContext { + public readonly contextChanged = new EventSource(); + + public collection: ICategoryCollection; + + public currentOs: OperatingSystem; + + public get state(): ICategoryCollectionState { + return this.states[this.collection.os]; + } + + private readonly states: StateMachine; + + public constructor( + public readonly app: IApplication, + initialContext: OperatingSystem, + ) { + this.states = initializeStates(app); + this.changeContext(initialContext); + } + + public changeContext(os: OperatingSystem): void { + assertInRange(os, OperatingSystem); + if (this.currentOs === os) { + return; + } + const collection = this.app.getCollection(os); + this.collection = collection; + const event: IApplicationContextChangedEvent = { + newState: this.states[os], + oldState: this.states[this.currentOs], + }; + this.contextChanged.notify(event); + this.currentOs = os; + } +} + +function initializeStates(app: IApplication): StateMachine { + const machine = new Map(); + for (const collection of app.collections) { + machine[collection.os] = new CategoryCollectionState(collection); + } + return machine; +} diff --git a/src/application/Context/ApplicationContextFactory.ts b/src/application/Context/ApplicationContextFactory.ts new file mode 100644 index 00000000..99f4344f --- /dev/null +++ b/src/application/Context/ApplicationContextFactory.ts @@ -0,0 +1,35 @@ +import type { IApplicationContext } from '@/application/Context/IApplicationContext'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import type { IApplication } from '@/domain/IApplication'; +import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory'; +import { ApplicationFactory } from '../ApplicationFactory'; +import { ApplicationContext } from './ApplicationContext'; +import type { IApplicationFactory } from '../IApplicationFactory'; + +export async function buildContext( + factory: IApplicationFactory = ApplicationFactory.Current, + environment = CurrentEnvironment, +): Promise { + const app = await factory.getApp(); + const os = getInitialOs(app, environment.os); + return new ApplicationContext(app, os); +} + +function getInitialOs( + app: IApplication, + currentOs: OperatingSystem | undefined, +): OperatingSystem { + const supportedOsList = app.getSupportedOsList(); + if (currentOs !== undefined && supportedOsList.includes(currentOs)) { + return currentOs; + } + return getMostSupportedOs(supportedOsList, app); +} + +function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) { + supportedOsList.sort((os1, os2) => { + const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; + return getPriority(os2) - getPriority(os1); + }); + return supportedOsList[0]; +} diff --git a/src/application/Context/IApplicationContext.ts b/src/application/Context/IApplicationContext.ts new file mode 100644 index 00000000..adabcf62 --- /dev/null +++ b/src/application/Context/IApplicationContext.ts @@ -0,0 +1,20 @@ +import { OperatingSystem } from '@/domain/OperatingSystem'; +import type { IEventSource } from '@/infrastructure/Events/IEventSource'; +import type { IApplication } from '@/domain/IApplication'; +import type { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState'; + +export interface IReadOnlyApplicationContext { + readonly app: IApplication; + readonly state: IReadOnlyCategoryCollectionState; + readonly contextChanged: IEventSource; +} + +export interface IApplicationContext extends IReadOnlyApplicationContext { + readonly state: ICategoryCollectionState; + changeContext(os: OperatingSystem): void; +} + +export interface IApplicationContextChangedEvent { + readonly newState: ICategoryCollectionState; + readonly oldState: ICategoryCollectionState; +} diff --git a/src/application/Context/State/CategoryCollectionState.ts b/src/application/Context/State/CategoryCollectionState.ts new file mode 100644 index 00000000..6d37ed8f --- /dev/null +++ b/src/application/Context/State/CategoryCollectionState.ts @@ -0,0 +1,51 @@ +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext'; +import { ApplicationCode } from './Code/ApplicationCode'; +import { UserSelectionFacade } from './Selection/UserSelectionFacade'; +import type { FilterContext } from './Filter/FilterContext'; +import type { UserSelection } from './Selection/UserSelection'; +import type { ICategoryCollectionState } from './ICategoryCollectionState'; +import type { IApplicationCode } from './Code/IApplicationCode'; + +export class CategoryCollectionState implements ICategoryCollectionState { + public readonly os: OperatingSystem; + + public readonly code: IApplicationCode; + + public readonly selection: UserSelection; + + public readonly filter: FilterContext; + + public constructor( + public readonly collection: ICategoryCollection, + selectionFactory = DefaultSelectionFactory, + codeFactory = DefaultCodeFactory, + filterFactory = DefaultFilterFactory, + ) { + this.selection = selectionFactory(collection, []); + this.code = codeFactory(this.selection.scripts, collection.scripting); + this.filter = filterFactory(collection); + this.os = collection.os; + } +} + +export type CodeFactory = ( + ...params: ConstructorParameters +) => IApplicationCode; + +const DefaultCodeFactory: CodeFactory = (...params) => new ApplicationCode(...params); + +export type SelectionFactory = ( + ...params: ConstructorParameters +) => UserSelection; + +const DefaultSelectionFactory: SelectionFactory = ( + ...params +) => new UserSelectionFacade(...params); + +export type FilterFactory = ( + ...params: ConstructorParameters +) => FilterContext; + +const DefaultFilterFactory: FilterFactory = (...params) => new AdaptiveFilterContext(...params); diff --git a/src/application/Context/State/Code/ApplicationCode.ts b/src/application/Context/State/Code/ApplicationCode.ts new file mode 100644 index 00000000..e376aadb --- /dev/null +++ b/src/application/Context/State/Code/ApplicationCode.ts @@ -0,0 +1,38 @@ +import { EventSource } from '@/infrastructure/Events/EventSource'; +import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import type { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; +import { CodeChangedEvent } from './Event/CodeChangedEvent'; +import { CodePosition } from './Position/CodePosition'; +import { UserScriptGenerator } from './Generation/UserScriptGenerator'; +import type { IUserScriptGenerator } from './Generation/IUserScriptGenerator'; +import type { ICodeChangedEvent } from './Event/ICodeChangedEvent'; +import type { IApplicationCode } from './IApplicationCode'; + +export class ApplicationCode implements IApplicationCode { + public readonly changed = new EventSource(); + + public current: string; + + private scriptPositions = new Map(); + + constructor( + selection: ReadonlyScriptSelection, + private readonly scriptingDefinition: IScriptingDefinition, + private readonly generator: IUserScriptGenerator = new UserScriptGenerator(), + ) { + this.setCode(selection.selectedScripts); + selection.changed.on((scripts) => { + this.setCode(scripts); + }); + } + + private setCode(scripts: ReadonlyArray): void { + const oldScripts = Array.from(this.scriptPositions.keys()); + const code = this.generator.buildCode(scripts, this.scriptingDefinition); + this.current = code.code; + this.scriptPositions = code.scriptPositions; + const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions); + this.changed.notify(event); + } +} diff --git a/src/application/Context/State/Code/Event/CodeChangedEvent.ts b/src/application/Context/State/Code/Event/CodeChangedEvent.ts new file mode 100644 index 00000000..7f19636f --- /dev/null +++ b/src/application/Context/State/Code/Event/CodeChangedEvent.ts @@ -0,0 +1,84 @@ +import type { Script } from '@/domain/Executables/Script/Script'; +import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; +import type { ICodeChangedEvent } from './ICodeChangedEvent'; + +export class CodeChangedEvent implements ICodeChangedEvent { + public readonly code: string; + + public readonly addedScripts: ReadonlyArray + + diff --git a/src/presentation/components/Code/CodeButtons/CodeCopyButton.vue b/src/presentation/components/Code/CodeButtons/CodeCopyButton.vue new file mode 100644 index 00000000..1da69814 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/CodeCopyButton.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/presentation/components/Code/CodeButtons/CodeRunButton.vue b/src/presentation/components/Code/CodeButtons/CodeRunButton.vue new file mode 100644 index 00000000..6c750296 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/CodeRunButton.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/presentation/components/Code/CodeButtons/IconButton.vue b/src/presentation/components/Code/CodeButtons/IconButton.vue new file mode 100644 index 00000000..0701495a --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/IconButton.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/CodeSaveButton.vue b/src/presentation/components/Code/CodeButtons/Save/CodeSaveButton.vue new file mode 100644 index 00000000..405af315 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/CodeSaveButton.vue @@ -0,0 +1,87 @@ + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Help/InfoTooltipInline.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Help/InfoTooltipInline.vue new file mode 100644 index 00000000..f5ad3f58 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Help/InfoTooltipInline.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Help/InfoTooltipWrapper.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Help/InfoTooltipWrapper.vue new file mode 100644 index 00000000..9e6e9c88 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Help/InfoTooltipWrapper.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/RunInstructions.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/RunInstructions.vue new file mode 100644 index 00000000..8ab15a67 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/RunInstructions.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue new file mode 100644 index 00000000..768a1061 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/InstructionStep.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/InstructionStep.vue new file mode 100644 index 00000000..2b9caf6c --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/InstructionStep.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/InstructionSteps.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/InstructionSteps.vue new file mode 100644 index 00000000..630baa0b --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/InstructionSteps.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/PlatformInstructionSteps.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/PlatformInstructionSteps.vue new file mode 100644 index 00000000..5e99fc3a --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/PlatformInstructionSteps.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue new file mode 100644 index 00000000..8523a2ad --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue @@ -0,0 +1,160 @@ + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue new file mode 100644 index 00000000..16f23098 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue new file mode 100644 index 00000000..037dd106 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue @@ -0,0 +1,159 @@ + + + diff --git a/src/presentation/components/Code/CodeButtons/ScriptErrorDialog.ts b/src/presentation/components/Code/CodeButtons/ScriptErrorDialog.ts new file mode 100644 index 00000000..2cd72fd1 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/ScriptErrorDialog.ts @@ -0,0 +1,239 @@ +import type { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner'; +import type { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import type { Dialog, SaveFileErrorType } from '@/presentation/common/Dialog'; + +type ErrorDialogParameters = Parameters; + +export async function createScriptErrorDialog( + information: ScriptErrorDetails, + scriptDiagnosticsCollector: ScriptDiagnosticsCollector | undefined, +): Promise { + const diagnostics = await scriptDiagnosticsCollector?.collectDiagnosticInformation(); + if (information.isFileReadbackError) { + return createAntivirusErrorDialog(information, diagnostics); + } + if (information.errorContext === 'run' + && information.errorType === 'ExternalProcessTermination') { + return createScriptInterruptedDialog(information); + } + return createGenericErrorDialog(information, diagnostics); +} + +export interface ScriptErrorDetails { + readonly errorContext: 'run' | 'save'; + readonly errorType: CodeRunErrorType | SaveFileErrorType; + readonly errorMessage: string; + readonly isFileReadbackError: boolean; +} + +function createGenericErrorDialog( + information: ScriptErrorDetails, + diagnostics: ScriptDiagnosticData | undefined, +): ErrorDialogParameters { + return [ + selectBasedOnErrorContext({ + runningScript: 'Error Running Script', + savingScript: 'Error Saving Script', + }, information), + [ + selectBasedOnErrorContext({ + runningScript: 'An error occurred while running the script.', + savingScript: 'An error occurred while saving the script.', + }, information), + 'This error could be caused by insufficient permissions, limited disk space, or security software interference.', + '\n', + generateUnorderedSolutionList({ + title: 'To address this, you can:', + solutions: [ + 'Check if there is enough disk space and system resources are available.', + selectBasedOnDirectoryPath({ + withoutDirectoryPath: 'Verify your access rights to the script\'s folder.', + withDirectoryPath: (directory) => `Verify your access rights to the script's folder: "${directory}".`, + }, diagnostics), + [ + 'Check if antivirus or security software has mistakenly blocked the script.', + 'Don\'t worry; privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software.', + 'Temporarily disabling the security software may resolve this.', + ].join(' '), + selectBasedOnErrorContext({ + runningScript: 'Confirm that you have the necessary permissions to execute scripts on your system.', + savingScript: 'Try saving the script to a different location.', + }, information), + generateTryDifferentSelectionAdvice(information), + 'If the problem persists, reach out to the community for further assistance.', + ], + }), + '\n', + generateTechnicalDetails(information), + ].join('\n'), + ]; +} + +function createAntivirusErrorDialog( + information: ScriptErrorDetails, + diagnostics: ScriptDiagnosticData | undefined, +): ErrorDialogParameters { + const defenderSteps = generateDefenderSteps(information, diagnostics); + return [ + 'Possible Antivirus Script Block', + [ + [ + 'It seems your antivirus software might have removed the script.', + 'Don\'t worry; privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software.', + ].join(' '), + '\n', + selectBasedOnErrorContext({ + savingScript: generateOrderedSolutionList({ + title: 'To address this, you can:', + solutions: [ + 'Check your antivirus for any blocking notifications and allow the script.', + 'Disable antivirus or security software temporarily or add an exclusion.', + 'Save the script again.', + ], + }), + runningScript: generateOrderedSolutionList({ + title: 'To address this, you can:', + solutions: [ + selectBasedOnDirectoryPath({ + withoutDirectoryPath: 'Disable antivirus or security software temporarily or add an exclusion.', + withDirectoryPath: (directory) => `Disable antivirus or security software temporarily or add a directory exclusion for scripts executed from: "${directory}".`, + }, diagnostics), + 'Run the script again.', + ], + }), + }, information), + defenderSteps ? `\n${defenderSteps}\n` : '\n', + [ + 'It\'s important to re-enable your antivirus protection after resolving the issue for your security.', + 'For more guidance, refer to your antivirus documentation.', + ].join(' '), + '\n', + generateUnorderedSolutionList({ + title: 'If the problem persists:', + solutions: [ + generateTryDifferentSelectionAdvice(information), + 'Consider reporting this as a false positive to your antivirus provider.', + 'Review your antivirus logs for more details.', + 'Reach out to the community for further assistance.', + ], + }), + '\n', + generateTechnicalDetails(information), + ].join('\n'), + ]; +} + +function createScriptInterruptedDialog( + information: ScriptErrorDetails, +): ErrorDialogParameters { + return [ + 'Script Stopped', + [ + 'The script stopped before it could finish.', + 'This happens if the script is cancelled manually or if the system terminates the process.', + '\n', + generateUnorderedSolutionList({ + title: 'To ensure successful script completion:', + solutions: [ + 'Keep the terminal window open during script execution.', + 'If the script closed unexpectedly, try running it again.', + 'Check for sufficient memory (RAM) and system resources.', + 'Avoid running tasks that might disrupt the script.', + ], + }), + '\n', + 'If you intentionally stopped the script, ignore this message.', + 'Reach out to the community for further assistance.', + '\n', + generateTechnicalDetails(information), + ].join('\n'), + ]; +} + +interface SolutionListOptions { + readonly solutions: readonly string[]; + readonly title: string; +} + +function generateUnorderedSolutionList(options: SolutionListOptions) { + return [ + options.title, + ...options.solutions.map((step) => `- ${step}`), + ].join('\n'); +} + +function generateTechnicalDetails(information: ScriptErrorDetails) { + const maxErrorMessageCharacters = 100; + const trimmedErrorMessage = information.errorMessage.length > maxErrorMessageCharacters + ? `${information.errorMessage.substring(0, maxErrorMessageCharacters - 3)}...` + : information.errorMessage; + return `Technical Details: [${information.errorType}] ${trimmedErrorMessage}`; +} + +function generateTryDifferentSelectionAdvice(information: ScriptErrorDetails) { + return selectBasedOnErrorContext({ + runningScript: 'Run a different script selection to check if the problem is script-specific.', + savingScript: 'Save a different script selection to check if the problem is script-specific.', + }, information); +} + +function selectBasedOnDirectoryPath( + options: { + readonly withoutDirectoryPath: T, + withDirectoryPath: (directoryPath: string) => T, + }, + diagnostics: ScriptDiagnosticData | undefined, +): T { + if (!diagnostics?.scriptsDirectoryAbsolutePath) { + return options.withoutDirectoryPath; + } + return options.withDirectoryPath(diagnostics.scriptsDirectoryAbsolutePath); +} + +function generateOrderedSolutionList(options: SolutionListOptions): string { + return [ + options.title, + ...options.solutions.map((step, index) => `${index + 1}. ${step}`), + ].join('\n'); +} + +function generateDefenderSteps( + information: ScriptErrorDetails, + diagnostics: ScriptDiagnosticData | undefined, +): string | undefined { + if (diagnostics?.currentOperatingSystem !== OperatingSystem.Windows) { + return undefined; + } + return generateOrderedSolutionList({ + title: 'To handle false warnings in Defender:', + solutions: [ + 'Open "Virus & threat protection" via the "Start" menu.', + 'Open "Manage settings" under "Virus & threat protection settings" heading.', + ...selectBasedOnErrorContext({ + savingScript: [ + 'Disable "Real-time protection" or add an exclusion by selecting "Add or remove exclusions".', + ], + runningScript: selectBasedOnDirectoryPath({ + withoutDirectoryPath: [ + 'Disable real-time protection or add exclusion for scripts.', + ], + withDirectoryPath: (directory) => [ + 'Open "Add or remove exclusions" under "Add or remove exclusions".', + `Add directory exclusion for "${directory}".`, + ], + }, diagnostics), + }, information), + ], + }); +} + +function selectBasedOnErrorContext(options: { + readonly savingScript: T; + readonly runningScript: T; +}, information: ScriptErrorDetails): T { + if (information.errorContext === 'run') { + return options.runningScript; + } + return options.savingScript; +} diff --git a/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue b/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue new file mode 100644 index 00000000..9cc1dc91 --- /dev/null +++ b/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/presentation/components/Code/TheCodeArea.vue b/src/presentation/components/Code/TheCodeArea.vue new file mode 100644 index 00000000..93edd884 --- /dev/null +++ b/src/presentation/components/Code/TheCodeArea.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/src/presentation/components/Code/ace-importer.ts b/src/presentation/components/Code/ace-importer.ts new file mode 100644 index 00000000..76978582 --- /dev/null +++ b/src/presentation/components/Code/ace-importer.ts @@ -0,0 +1,17 @@ +import ace from 'ace-builds'; + +/* + Following is here because `import 'ace-builds/esm-resolver' imports all unused functionality + when built with Vite (`npm run build`). +*/ + +import 'ace-builds/src-noconflict/theme-github'; +import 'ace-builds/src-noconflict/theme-xcode'; +import 'ace-builds/src-noconflict/mode-batchfile'; +import 'ace-builds/src-noconflict/mode-sh'; + +ace.config.setModuleUrl('ace/mode/html_worker', new URL('ace-builds/src-noconflict/worker-html.js', import.meta.url).toString()); +ace.config.setModuleUrl('ace/mode/javascript_worker', new URL('ace-builds/src-noconflict/worker-javascript.js', import.meta.url).toString()); +ace.config.setModuleUrl('ace/mode/json_worker', new URL('ace-builds/src-noconflict/worker-json.js', import.meta.url).toString()); + +export default ace; diff --git a/src/presentation/components/DevToolkit/DevToolkit.vue b/src/presentation/components/DevToolkit/DevToolkit.vue new file mode 100644 index 00000000..b130bf8c --- /dev/null +++ b/src/presentation/components/DevToolkit/DevToolkit.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/src/presentation/components/DevToolkit/DumpNames.ts b/src/presentation/components/DevToolkit/DumpNames.ts new file mode 100644 index 00000000..6b9e6711 --- /dev/null +++ b/src/presentation/components/DevToolkit/DumpNames.ts @@ -0,0 +1,35 @@ +import type { IApplication } from '@/domain/IApplication'; +import { ApplicationFactory } from '@/application/ApplicationFactory'; + +export async function dumpNames(): Promise { + const application = await ApplicationFactory.Current.getApp(); + const names = collectNames(application); + const output = names.join('\n'); + return output; +} + +function collectNames(application: IApplication): string[] { + const { collections } = application; + + const allNames = [ + ...collections.flatMap((collection) => collection.getAllCategories().map((c) => c.name)), + ...collections.flatMap((collection) => collection.getAllScripts().map((c) => c.name)), + ]; + + const uniqueNames = [...new Set(allNames)]; + + return shuffle(uniqueNames); +} + +/* + Shuffle an array of strings, returning a new array with elements in random order. + Uses the Fisher-Yates (or Durstenfeld) algorithm. +*/ +function shuffle(array: readonly string[]): string[] { + const shuffledArray = [...array]; + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; + } + return shuffledArray; +} diff --git a/src/presentation/components/DevToolkit/UseScrollbarGutterWidth.ts b/src/presentation/components/DevToolkit/UseScrollbarGutterWidth.ts new file mode 100644 index 00000000..b96e8f52 --- /dev/null +++ b/src/presentation/components/DevToolkit/UseScrollbarGutterWidth.ts @@ -0,0 +1,46 @@ +import { + computed, readonly, ref, shallowRef, watch, +} from 'vue'; +import { throttle } from '@/application/Common/Timing/Throttle'; +import { useAutoUnsubscribedEventListener } from '../Shared/Hooks/UseAutoUnsubscribedEventListener'; +import { useResizeObserver } from '../Shared/Hooks/Resize/UseResizeObserver'; + +const RESIZE_EVENT_THROTTLE_MS = 200; + +export function useScrollbarGutterWidth() { + const scrollbarWidthInPx = ref(getScrollbarGutterWidth()); + + const { startListening } = useAutoUnsubscribedEventListener(); + startListening(window, 'resize', throttle(() => { + scrollbarWidthInPx.value = getScrollbarGutterWidth(); + }, RESIZE_EVENT_THROTTLE_MS)); + + const bodyWidth = useBodyWidth(); + watch(() => bodyWidth.value, () => { + scrollbarWidthInPx.value = getScrollbarGutterWidth(); + }, { immediate: false }); + + const scrollbarWidthStyle = computed(() => `${scrollbarWidthInPx.value}px`); + return readonly(scrollbarWidthStyle); +} + +function getScrollbarGutterWidth(): number { + return document.documentElement.clientWidth - document.documentElement.offsetWidth; +} + +function useBodyWidth() { + const width = ref(document.body.offsetWidth); + useResizeObserver( + { + observedElementRef: shallowRef(document.body), + throttleInMs: RESIZE_EVENT_THROTTLE_MS, + observeCallback: (entries) => { + for (const entry of entries) { + width.value = entry.borderBoxSize[0].inlineSize; + } + }, + observeOptions: { box: 'border-box' }, + }, + ); + return readonly(width); +} diff --git a/src/presentation/components/Scripts/Menu/MenuOptionList.vue b/src/presentation/components/Scripts/Menu/MenuOptionList.vue new file mode 100644 index 00000000..6fbc6126 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/MenuOptionList.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/presentation/components/Scripts/Menu/MenuOptionListItem.vue b/src/presentation/components/Scripts/Menu/MenuOptionListItem.vue new file mode 100644 index 00000000..3c6155e0 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/MenuOptionListItem.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/presentation/components/Scripts/Menu/Recommendation/Rating/CircleRating.vue b/src/presentation/components/Scripts/Menu/Recommendation/Rating/CircleRating.vue new file mode 100644 index 00000000..7232bc0b --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Recommendation/Rating/CircleRating.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/presentation/components/Scripts/Menu/Recommendation/Rating/RatingCircle.vue b/src/presentation/components/Scripts/Menu/Recommendation/Rating/RatingCircle.vue new file mode 100644 index 00000000..67b89832 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Recommendation/Rating/RatingCircle.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue new file mode 100644 index 00000000..9daff501 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusHandler.ts b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusHandler.ts new file mode 100644 index 00000000..5eaff647 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationStatusHandler.ts @@ -0,0 +1,104 @@ +import type { Script } from '@/domain/Executables/Script/Script'; +import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; +import { scrambledEqual } from '@/application/Common/Array'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { ReadonlyScriptSelection, ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import { RecommendationStatusType } from './RecommendationStatusType'; + +export function setCurrentRecommendationStatus( + type: RecommendationStatusType, + context: SelectionMutationContext, +) { + if (type === RecommendationStatusType.Custom) { + throw new Error('Cannot select custom type.'); + } + const selector = selectors.get(type); + if (!selector) { + throw new Error(`Cannot handle the type: ${RecommendationStatusType[type]}`); + } + selector.select(context); +} + +export function getCurrentRecommendationStatus( + context: SelectionCheckContext, +): RecommendationStatusType { + for (const [type, selector] of selectors.entries()) { + if (selector.isSelected(context)) { + return type; + } + } + return RecommendationStatusType.Custom; +} + +export interface SelectionCheckContext { + readonly selection: ReadonlyScriptSelection; + readonly collection: ICategoryCollection; +} + +export interface SelectionMutationContext { + readonly selection: ScriptSelection, + readonly collection: ICategoryCollection, +} + +interface RecommendationStatusTypeHandler { + isSelected: (context: SelectionCheckContext) => boolean; + select: (context: SelectionMutationContext) => void; +} + +const selectors = new Map([ + [RecommendationStatusType.None, { + select: ({ selection }) => selection.deselectAll(), + isSelected: ({ selection }) => selection.selectedScripts.length === 0, + }], + [RecommendationStatusType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)], + [RecommendationStatusType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)], + [RecommendationStatusType.All, { + select: ({ selection }) => selection.selectAll(), + isSelected: ( + { selection, collection }, + ) => selection.selectedScripts.length === collection.totalScripts, + }], +]); + +function getRecommendationLevelSelector( + level: RecommendationLevel, +): RecommendationStatusTypeHandler { + return { + select: (context) => selectOnly(level, context), + isSelected: (context) => hasAllSelectedLevelOf(level, context), + }; +} + +function hasAllSelectedLevelOf( + level: RecommendationLevel, + context: SelectionCheckContext, +): boolean { + const { collection, selection } = context; + const scripts = collection.getScriptsByLevel(level); + const { selectedScripts } = selection; + return areAllSelected(scripts, selectedScripts); +} + +function selectOnly( + level: RecommendationLevel, + context: SelectionMutationContext, +): void { + const { collection, selection } = context; + const scripts = collection.getScriptsByLevel(level); + selection.selectOnly(scripts); +} + +function areAllSelected( + expectedScripts: ReadonlyArray diff --git a/src/presentation/components/Scripts/Menu/Revert/RevertStatusDocumentation.vue b/src/presentation/components/Scripts/Menu/Revert/RevertStatusDocumentation.vue new file mode 100644 index 00000000..bdb8450c --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Revert/RevertStatusDocumentation.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/src/presentation/components/Scripts/Menu/Revert/RevertStatusHandler.ts b/src/presentation/components/Scripts/Menu/Revert/RevertStatusHandler.ts new file mode 100644 index 00000000..a9d6aaf6 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Revert/RevertStatusHandler.ts @@ -0,0 +1,79 @@ +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import type { ReadonlyScriptSelection, ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; +import type { ScriptSelectionChange } from '@/application/Context/State/Selection/Script/ScriptSelectionChange'; +import { RevertStatusType } from './RevertStatusType'; + +export function setCurrentRevertStatus( + desiredRevertStatus: boolean, + selection: ScriptSelection, +) { + const scriptRevertStatusChanges = getScriptRevertStatusChanges( + selection.selectedScripts, + desiredRevertStatus, + ); + if (scriptRevertStatusChanges.length === 0) { + return; + } + selection.processChanges({ changes: scriptRevertStatusChanges }); +} + +export function getCurrentRevertStatus( + selection: ReadonlyScriptSelection, +): RevertStatusType { + const allScriptRevertStatuses = filterReversibleScripts(selection.selectedScripts) + .map((selectedScript) => selectedScript.revert); + if (!allScriptRevertStatuses.length) { + return RevertStatusType.NoReversibleScripts; + } + if (allScriptRevertStatuses.every((revertStatus) => revertStatus)) { + return RevertStatusType.AllScriptsReverted; + } + if (allScriptRevertStatuses.every((revertStatus) => !revertStatus)) { + return RevertStatusType.NoScriptsReverted; + } + return RevertStatusType.SomeScriptsReverted; +} + +function getScriptRevertStatusChanges( + selectedScripts: readonly SelectedScript[], + desiredRevertStatus: boolean, +): ScriptSelectionChange[] { + const reversibleSelectedScripts = filterReversibleScripts(selectedScripts); + const selectedScriptsRequiringChange = filterScriptsRequiringRevertStatusChange( + reversibleSelectedScripts, + desiredRevertStatus, + ); + const revertStatusChanges = mapToScriptSelectionChanges( + selectedScriptsRequiringChange, + desiredRevertStatus, + ); + return revertStatusChanges; +} + +function filterReversibleScripts(selectedScripts: readonly SelectedScript[]) { + return selectedScripts.filter( + (selectedScript) => selectedScript.script.canRevert(), + ); +} + +function filterScriptsRequiringRevertStatusChange( + selectedScripts: readonly SelectedScript[], + desiredRevertStatus: boolean, +) { + return selectedScripts.filter( + (selectedScript) => selectedScript.revert !== desiredRevertStatus, + ); +} + +function mapToScriptSelectionChanges( + scriptsNeedingChange: readonly SelectedScript[], + newRevertStatus: boolean, +): ScriptSelectionChange[] { + return scriptsNeedingChange.map((script): ScriptSelectionChange => ({ + scriptId: script.id, + newStatus: { + isSelected: true, + isReverted: newRevertStatus, + }, + })); +} diff --git a/src/presentation/components/Scripts/Menu/Revert/RevertStatusType.ts b/src/presentation/components/Scripts/Menu/Revert/RevertStatusType.ts new file mode 100644 index 00000000..d77a8ebf --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Revert/RevertStatusType.ts @@ -0,0 +1,6 @@ +export enum RevertStatusType { + NoReversibleScripts, + NoScriptsReverted, + SomeScriptsReverted, + AllScriptsReverted, +} diff --git a/src/presentation/components/Scripts/Menu/Revert/TheRevertSelector.vue b/src/presentation/components/Scripts/Menu/Revert/TheRevertSelector.vue new file mode 100644 index 00000000..22414caa --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Revert/TheRevertSelector.vue @@ -0,0 +1,145 @@ + + + diff --git a/src/presentation/components/Scripts/Menu/TheOsChanger.vue b/src/presentation/components/Scripts/Menu/TheOsChanger.vue new file mode 100644 index 00000000..e0cf643a --- /dev/null +++ b/src/presentation/components/Scripts/Menu/TheOsChanger.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue new file mode 100644 index 00000000..6baae0cd --- /dev/null +++ b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/src/presentation/components/Scripts/Menu/View/TheViewChanger.vue b/src/presentation/components/Scripts/Menu/View/TheViewChanger.vue new file mode 100644 index 00000000..e54e3d37 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/View/TheViewChanger.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/presentation/components/Scripts/Menu/View/ViewType.ts b/src/presentation/components/Scripts/Menu/View/ViewType.ts new file mode 100644 index 00000000..c926c591 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/View/ViewType.ts @@ -0,0 +1,4 @@ +export enum ViewType { + Cards = 1, + Tree = 0, +} diff --git a/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue b/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue new file mode 100644 index 00000000..fa021dc3 --- /dev/null +++ b/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/presentation/components/Scripts/Slider/SliderHandle.vue b/src/presentation/components/Scripts/Slider/SliderHandle.vue new file mode 100644 index 00000000..a6c9a2a7 --- /dev/null +++ b/src/presentation/components/Scripts/Slider/SliderHandle.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/presentation/components/Scripts/Slider/UseDragHandler.ts b/src/presentation/components/Scripts/Slider/UseDragHandler.ts new file mode 100644 index 00000000..8d900072 --- /dev/null +++ b/src/presentation/components/Scripts/Slider/UseDragHandler.ts @@ -0,0 +1,92 @@ +import { + onUnmounted, ref, shallowReadonly, watch, +} from 'vue'; +import { throttle } from '@/application/Common/Timing/Throttle'; +import type { Ref } from 'vue'; +import type { LifecycleHook } from '../../Shared/Hooks/Common/LifecycleHook'; + +const ThrottleInMs = 15; + +export function useDragHandler( + draggableElementRef: Readonly>, + dragDomModifier: DragDomModifier = new GlobalDocumentDragDomModifier(), + throttler = throttle, + onTeardown: LifecycleHook = onUnmounted, +) { + const displacementX = ref(0); + const isDragging = ref(false); + + let initialPointerX: number | undefined; + + const onDrag = throttler((event: PointerEvent) => { + if (initialPointerX === undefined) { + throw new Error('Resize action started without an initial X coordinate.'); + } + displacementX.value = event.clientX - initialPointerX; + initialPointerX = event.clientX; + }, ThrottleInMs); + + const stopDrag = () => { + isDragging.value = false; + dragDomModifier.removeEventListenerFromDocument('pointermove', onDrag); + dragDomModifier.removeEventListenerFromDocument('pointerup', stopDrag); + }; + + const startDrag = (event: PointerEvent) => { + isDragging.value = true; + initialPointerX = event.clientX; + dragDomModifier.addEventListenerToDocument('pointermove', onDrag); + dragDomModifier.addEventListenerToDocument('pointerup', stopDrag); + event.stopPropagation(); + event.preventDefault(); + }; + + watch(draggableElementRef, (element) => { + if (!element) { + initialPointerX = undefined; + return; + } + initializeElement(element); + }, { immediate: true }); + + function initializeElement(element: HTMLElement) { + element.style.touchAction = 'none'; // Disable default touch behavior, necessary for resizing functionality to work correctly on touch-enabled devices + element.addEventListener('pointerdown', startDrag); + } + + onTeardown(() => { + stopDrag(); + }); + + return { + displacementX: shallowReadonly(displacementX), + isDragging: shallowReadonly(isDragging), + }; +} + +export interface DragDomModifier { + addEventListenerToDocument( + type: keyof DocumentEventMap, + handler: EventListener, + ): void; + removeEventListenerFromDocument( + type: keyof DocumentEventMap, + handler: EventListener, + ): void; +} + +class GlobalDocumentDragDomModifier implements DragDomModifier { + public addEventListenerToDocument( + type: keyof DocumentEventMap, + listener: EventListener, + ): void { + document.addEventListener(type, listener); + } + + public removeEventListenerFromDocument( + type: keyof DocumentEventMap, + listener: EventListener, + ): void { + document.removeEventListener(type, listener); + } +} diff --git a/src/presentation/components/Scripts/Slider/UseGlobalCursor.ts b/src/presentation/components/Scripts/Slider/UseGlobalCursor.ts new file mode 100644 index 00000000..d8dbb9c2 --- /dev/null +++ b/src/presentation/components/Scripts/Slider/UseGlobalCursor.ts @@ -0,0 +1,54 @@ +import { watch, type Ref, onUnmounted } from 'vue'; +import type { LifecycleHook } from '../../Shared/Hooks/Common/LifecycleHook'; + +export function useGlobalCursor( + isActive: Readonly>, + cursorCssValue: string, + documentAccessor: CursorStyleDomModifier = new GlobalDocumentCursorStyleDomModifier(), + onTeardown: LifecycleHook = onUnmounted, +) { + const cursorStyle = createCursorStyle(cursorCssValue, documentAccessor); + + watch(isActive, (isCursorVisible) => { + if (isCursorVisible) { + documentAccessor.appendStyleToHead(cursorStyle); + } else { + documentAccessor.removeElement(cursorStyle); + } + }); + + onTeardown(() => { + documentAccessor.removeElement(cursorStyle); + }); +} + +function createCursorStyle( + cursorCssValue: string, + documentAccessor: CursorStyleDomModifier, +): HTMLStyleElement { + // Using `document.body.style.cursor` does not override cursor when hovered on input boxes, + // buttons etc. so we create a custom style that will do that + const cursorStyle = documentAccessor.createStyleElement(); + cursorStyle.innerHTML = `*{cursor: ${cursorCssValue}!important;}`; + return cursorStyle; +} + +export interface CursorStyleDomModifier { + appendStyleToHead(element: HTMLStyleElement): void; + removeElement(element: HTMLStyleElement): void; + createStyleElement(): HTMLStyleElement; +} + +class GlobalDocumentCursorStyleDomModifier implements CursorStyleDomModifier { + public appendStyleToHead(element: HTMLStyleElement): void { + document.head.appendChild(element); + } + + public removeElement(element: HTMLStyleElement): void { + element.remove(); + } + + public createStyleElement(): HTMLStyleElement { + return document.createElement('style'); + } +} diff --git a/src/presentation/components/Scripts/TheScriptArea.vue b/src/presentation/components/Scripts/TheScriptArea.vue new file mode 100644 index 00000000..0a88456e --- /dev/null +++ b/src/presentation/components/Scripts/TheScriptArea.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Cards/CardExpandTransition.vue b/src/presentation/components/Scripts/View/Cards/CardExpandTransition.vue new file mode 100644 index 00000000..f5fdcecb --- /dev/null +++ b/src/presentation/components/Scripts/View/Cards/CardExpandTransition.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Cards/CardExpansionArrow.vue b/src/presentation/components/Scripts/View/Cards/CardExpansionArrow.vue new file mode 100644 index 00000000..2871b507 --- /dev/null +++ b/src/presentation/components/Scripts/View/Cards/CardExpansionArrow.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Cards/CardList.vue b/src/presentation/components/Scripts/View/Cards/CardList.vue new file mode 100644 index 00000000..f054265e --- /dev/null +++ b/src/presentation/components/Scripts/View/Cards/CardList.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Cards/CardListItem.vue b/src/presentation/components/Scripts/View/Cards/CardListItem.vue new file mode 100644 index 00000000..74c175f3 --- /dev/null +++ b/src/presentation/components/Scripts/View/Cards/CardListItem.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Cards/CardSelectionIndicator.vue b/src/presentation/components/Scripts/View/Cards/CardSelectionIndicator.vue new file mode 100644 index 00000000..3665e517 --- /dev/null +++ b/src/presentation/components/Scripts/View/Cards/CardSelectionIndicator.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Cards/NonCollapsingDirective.ts b/src/presentation/components/Scripts/View/Cards/NonCollapsingDirective.ts new file mode 100644 index 00000000..6c9631c0 --- /dev/null +++ b/src/presentation/components/Scripts/View/Cards/NonCollapsingDirective.ts @@ -0,0 +1,17 @@ +import type { ObjectDirective } from 'vue'; + +const attributeName = 'data-interaction-does-not-collapse'; + +export function hasDirective(el: Element): boolean { + if (el.hasAttribute(attributeName)) { + return true; + } + const parent = el.closest(`[${attributeName}]`); + return !!parent; +} + +export const NonCollapsing: ObjectDirective = { + mounted(el: HTMLElement) { + el.setAttribute(attributeName, ''); + }, +}; diff --git a/src/presentation/components/Scripts/View/Cards/card-gap.scss b/src/presentation/components/Scripts/View/Cards/card-gap.scss new file mode 100644 index 00000000..f46a7e00 --- /dev/null +++ b/src/presentation/components/Scripts/View/Cards/card-gap.scss @@ -0,0 +1,3 @@ +@use "@/presentation/assets/styles/main" as *; + +$card-gap: $spacing-absolute-large; diff --git a/src/presentation/components/Scripts/View/TheScriptsView.vue b/src/presentation/components/Scripts/View/TheScriptsView.vue new file mode 100644 index 00000000..7bbac2aa --- /dev/null +++ b/src/presentation/components/Scripts/View/TheScriptsView.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentableNode.vue b/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentableNode.vue new file mode 100644 index 00000000..a5de5e34 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentableNode.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentationText.vue b/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentationText.vue new file mode 100644 index 00000000..854c1a7b --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/DocumentationText.vue @@ -0,0 +1,65 @@ + + + + + +@/application/Text/SplitTextIntoLines diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/ToggleDocumentationButton.vue b/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/ToggleDocumentationButton.vue new file mode 100644 index 00000000..bfcc1723 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Documentation/ToggleDocumentationButton.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer.ts new file mode 100644 index 00000000..1d55a325 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer.ts @@ -0,0 +1,28 @@ +import { InlineReferenceLabelsToSuperscriptConverter } from './Renderers/InlineReferenceLabelsToSuperscriptConverter'; +import { MarkdownItHtmlRenderer } from './Renderers/MarkdownItHtmlRenderer'; +import { PlainTextUrlsToHyperlinksConverter } from './Renderers/PlainTextUrlsToHyperlinksConverter'; +import type { MarkdownRenderer } from './MarkdownRenderer'; + +export class CompositeMarkdownRenderer implements MarkdownRenderer { + constructor( + private readonly renderers: readonly MarkdownRenderer[] = StandardMarkdownRenderers, + ) { + if (!renderers.length) { + throw new Error('missing renderers'); + } + } + + public render(markdownContent: string): string { + let renderedContent = markdownContent; + for (const renderer of this.renderers) { + renderedContent = renderer.render(renderedContent); + } + return renderedContent; + } +} + +const StandardMarkdownRenderers: readonly MarkdownRenderer[] = [ + new PlainTextUrlsToHyperlinksConverter(), + new InlineReferenceLabelsToSuperscriptConverter(), + new MarkdownItHtmlRenderer(), +] as const; diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/MarkdownRenderer.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/MarkdownRenderer.ts new file mode 100644 index 00000000..6a4b62c3 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/MarkdownRenderer.ts @@ -0,0 +1,3 @@ +export interface MarkdownRenderer { + render(markdownContent: string): string; +} diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/MarkdownText.vue b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/MarkdownText.vue new file mode 100644 index 00000000..0f6c5d15 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/MarkdownText.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/InlineReferenceLabelsToSuperscriptConverter.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/InlineReferenceLabelsToSuperscriptConverter.ts new file mode 100644 index 00000000..c05fa979 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/InlineReferenceLabelsToSuperscriptConverter.ts @@ -0,0 +1,36 @@ +import type { MarkdownRenderer } from '../MarkdownRenderer'; + +export class InlineReferenceLabelsToSuperscriptConverter implements MarkdownRenderer { + public render(markdownContent: string): string { + return convertInlineReferenceLabelsToSuperscript(markdownContent); + } +} + +function convertInlineReferenceLabelsToSuperscript(content: string): string { + if (!content) { + return content; + } + return content.replaceAll(TextInsideBracketsPattern, (_fullMatch, label, offset) => { + if (!isInlineReferenceLabel(label, content, offset)) { + return `[${label}]`; + } + return `[${label}]`; + }); +} + +function isInlineReferenceLabel( + referenceLabel: string, + markdownText: string, + openingBracketPosition: number, +): boolean { + const referenceLabelDefinitionIndex = markdownText.indexOf(`\n[${referenceLabel}]: `); + if (openingBracketPosition - 1 /* -1 for newline */ === referenceLabelDefinitionIndex) { + return false; // It is a reference definition, not a label. + } + if (referenceLabelDefinitionIndex === -1) { + return false; // The reference definition is missing. + } + return true; +} + +const TextInsideBracketsPattern = /\[(.*?)\]/gm; diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/MarkdownItHtmlRenderer.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/MarkdownItHtmlRenderer.ts new file mode 100644 index 00000000..076edc0a --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/MarkdownItHtmlRenderer.ts @@ -0,0 +1,40 @@ +import MarkdownIt from 'markdown-it'; +import type { MarkdownRenderer } from '../MarkdownRenderer'; +import type { RenderRule } from 'markdown-it/lib/renderer.mjs'; // eslint-disable-line import/extensions + +export class MarkdownItHtmlRenderer implements MarkdownRenderer { + public render(markdownContent: string): string { + const markdownParser = new MarkdownIt({ + html: true, // Enable HTML tags in source to allow other custom rendering logic. + linkify: false, // Disables auto-linking; handled manually for custom formatting. + breaks: false, // Disables conversion of single newlines (`\n`) to HTML breaks (`
`). + }); + configureLinksToOpenInNewTab(markdownParser); + return markdownParser.render(markdownContent); + } +} + +function configureLinksToOpenInNewTab(markdownParser: MarkdownIt): void { + // https://github.com/markdown-it/markdown-it/blob/14.0.0/docs/architecture.md#renderer + const defaultLinkRenderer = getDefaultRenderer(markdownParser, 'link_open'); + markdownParser.renderer.rules.link_open = (tokens, index, options, env, self) => { + const currentToken = tokens[index]; + Object.entries(AnchorAttributesForExternalLinks).forEach(([attribute, value]) => { + currentToken.attrSet(attribute, value); + }); + return defaultLinkRenderer(tokens, index, options, env, self); + }; +} + +function getDefaultRenderer(md: MarkdownIt, ruleName: string): RenderRule { + const ruleRenderer = md.renderer.rules[ruleName]; + const renderTokenAsDefault: RenderRule = (tokens, idx, options, _env, self) => { + return self.renderToken(tokens, idx, options); + }; + return ruleRenderer || renderTokenAsDefault; +} + +const AnchorAttributesForExternalLinks: Record = { + target: '_blank', + rel: 'noopener noreferrer', +} as const; diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/PlainTextUrlsToHyperlinksConverter.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/PlainTextUrlsToHyperlinksConverter.ts new file mode 100644 index 00000000..3d30cff9 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/Renderers/PlainTextUrlsToHyperlinksConverter.ts @@ -0,0 +1,127 @@ +import type { MarkdownRenderer } from '../MarkdownRenderer'; + +export class PlainTextUrlsToHyperlinksConverter implements MarkdownRenderer { + public render(markdownContent: string): string { + return autoLinkPlainUrls(markdownContent); + } +} + +const PlainTextUrlInMarkdownRegex = /(? { + return fullMatch.replace(url, formatReadableLink(url)); + }); +} + +function formatReadableLink(url: string): string { + const urlParts = new URL(url); + let displayText = formatHostName(urlParts.hostname); + const pageLabel = extractPageLabel(urlParts); + if (pageLabel) { + displayText += ` - ${truncateTextFromEnd(capitalizeEachWord(pageLabel), 50)}`; + } + return buildMarkdownLink(displayText, url); +} + +function formatHostName(hostname: string): string { + const withoutWww = hostname.replace(/^(www\.)/, ''); + const truncatedHostName = truncateTextFromStart(withoutWww, 30); + return truncatedHostName; +} + +function extractPageLabel(urlParts: URL): string | undefined { + const readablePath = makePathReadable(urlParts.pathname); + if (readablePath) { + return readablePath; + } + return formatQueryParameters(urlParts.searchParams); +} + +function buildMarkdownLink(label: string, url: string): string { + return `[${label}](${url})`; +} + +function formatQueryParameters(queryParameters: URLSearchParams): string | undefined { + const queryValues = [...queryParameters.values()]; + if (queryValues.length === 0) { + return undefined; + } + return findMostDescriptiveName(queryValues); +} + +function truncateTextFromStart(text: string, maxLength: number): string { + return text.length > maxLength ? `…${text.substring(text.length - maxLength)}` : text; +} + +function truncateTextFromEnd(text: string, maxLength: number): string { + return text.length > maxLength ? `${text.substring(0, maxLength)}…` : text; +} + +function isNumeric(value: string): boolean { + return /^\d+$/.test(value); +} + +function makePathReadable(path: string): string | undefined { + const decodedPath = decodeURI(path); // Decode URI components, e.g., '%20' to space + const pathParts = decodedPath.split('/'); + const decodedPathParts = pathParts // Split then decode to correctly handle '%2F' as '/' + .map((pathPart) => decodeURIComponent(pathPart)); + const descriptivePart = findMostDescriptiveName(decodedPathParts); + if (!descriptivePart) { + return undefined; + } + const withoutExtension = removeFileExtension(descriptivePart); + const tokenizedText = tokenizeTextForReadability(withoutExtension); + return tokenizedText; +} + +function tokenizeTextForReadability(text: string): string { + return text + .replaceAll(/[-_+]/g, ' ') // Replace hyphens, underscores, and plus signs with spaces + .replaceAll(/\s+/g, ' '); // Collapse multiple consecutive spaces into a single space +} + +function removeFileExtension(value: string): string { + const parts = value.split('.'); + if (parts.length === 1) { + return value; + } + const extension = parts[parts.length - 1]; + if (extension.length > 9) { + return value; // Heuristically web file extension is no longer than 9 chars (e.g. "html") + } + return parts.slice(0, -1).join('.'); +} + +function capitalizeEachWord(text: string): string { + return text + .split(' ') + .map((word) => capitalizeFirstLetter(word)) + .join(' '); +} + +function capitalizeFirstLetter(text: string): string { + return text.charAt(0).toUpperCase() + text.slice(1); +} + +function findMostDescriptiveName(segments: readonly string[]): string | undefined { + const meaningfulSegments = segments.filter(isMeaningfulPathSegment); + if (meaningfulSegments.length === 0) { + return undefined; + } + const longestGoodSegment = meaningfulSegments.reduce((a, b) => (a.length > b.length ? a : b)); + return longestGoodSegment; +} + +function isMeaningfulPathSegment(segment: string): boolean { + return segment.length > 2 // This is often non-human categories like T5 etc. + && !isNumeric(segment) // E.g. article numbers, issue numbers + && !/^index(?:\.\S{0,10}$|$)/.test(segment) // E.g. index.html + && !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(segment) // Locale string e.g. fr-FR + && !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(segment) // GUID + && !/^[0-9a-f]{40}$/.test(segment); // Git SHA (e.g. GitHub links) +} diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/NodeContent.vue b/src/presentation/components/Scripts/View/Tree/NodeContent/NodeContent.vue new file mode 100644 index 00000000..06f26429 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/NodeContent.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata.ts new file mode 100644 index 00000000..b086294e --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata.ts @@ -0,0 +1,15 @@ +import type { ExecutableId } from '@/domain/Executables/Identifiable'; + +export enum NodeType { + Script, + Category, +} + +export interface NodeMetadata { + readonly executableId: ExecutableId; + readonly text: string; + readonly isReversible: boolean; + readonly docs: ReadonlyArray; + readonly children: ReadonlyArray; + readonly type: NodeType; +} diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/NodeTitle.vue b/src/presentation/components/Scripts/View/Tree/NodeContent/NodeTitle.vue new file mode 100644 index 00000000..42e4d569 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/NodeTitle.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/RevertToggle.vue b/src/presentation/components/Scripts/View/Tree/NodeContent/RevertToggle.vue new file mode 100644 index 00000000..34695ff8 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/RevertToggle.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts new file mode 100644 index 00000000..e13affa4 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts @@ -0,0 +1,50 @@ +import type { UserSelection } from '@/application/Context/State/Selection/UserSelection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; +import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter'; +import { ScriptReverter } from './ScriptReverter'; +import type { Reverter } from './Reverter'; +import type { TreeNodeId } from '../../TreeView/Node/TreeNode'; + +export class CategoryReverter implements Reverter { + private readonly categoryId: ExecutableId; + + private readonly scriptReverters: ReadonlyArray; + + constructor(nodeId: TreeNodeId, collection: ICategoryCollection) { + this.categoryId = createExecutableIdFromNodeId(nodeId); + this.scriptReverters = createScriptReverters(this.categoryId, collection); + } + + public getState(selectedScripts: ReadonlyArray): boolean { + if (!this.scriptReverters.length) { + // An empty array indicates there are no reversible scripts in this category + return false; + } + return this.scriptReverters.every((script) => script.getState(selectedScripts)); + } + + public selectWithRevertState(newState: boolean, selection: UserSelection): void { + selection.categories.processChanges({ + changes: [{ + categoryId: this.categoryId, + newStatus: { + isSelected: true, + isReverted: newState, + }, + }], + }); + } +} + +function createScriptReverters( + categoryId: ExecutableId, + collection: ICategoryCollection, +): ScriptReverter[] { + const category = collection.getCategory(categoryId); + const scripts = category + .getAllScriptsRecursively() + .filter((script) => script.canRevert()); + return scripts.map((script) => new ScriptReverter(script.executableId)); +} diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/Reverter.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/Reverter.ts new file mode 100644 index 00000000..4916eb88 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/Reverter.ts @@ -0,0 +1,7 @@ +import type { UserSelection } from '@/application/Context/State/Selection/UserSelection'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; + +export interface Reverter { + getState(selectedScripts: ReadonlyArray): boolean; + selectWithRevertState(newState: boolean, selection: UserSelection): void; +} diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts new file mode 100644 index 00000000..a2a64503 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts @@ -0,0 +1,19 @@ +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import { type NodeMetadata, NodeType } from '../NodeMetadata'; +import { ScriptReverter } from './ScriptReverter'; +import { CategoryReverter } from './CategoryReverter'; +import type { Reverter } from './Reverter'; + +export function getReverter( + node: NodeMetadata, + collection: ICategoryCollection, +): Reverter { + switch (node.type) { + case NodeType.Category: + return new CategoryReverter(node.executableId, collection); + case NodeType.Script: + return new ScriptReverter(node.executableId); + default: + throw new Error('Unknown script type'); + } +} diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts new file mode 100644 index 00000000..629b2037 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts @@ -0,0 +1,34 @@ +import type { UserSelection } from '@/application/Context/State/Selection/UserSelection'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; +import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter'; +import type { Reverter } from './Reverter'; +import type { TreeNodeId } from '../../TreeView/Node/TreeNode'; + +export class ScriptReverter implements Reverter { + private readonly scriptId: ExecutableId; + + constructor(nodeId: TreeNodeId) { + this.scriptId = createExecutableIdFromNodeId(nodeId); + } + + public getState(selectedScripts: ReadonlyArray): boolean { + const selectedScript = selectedScripts.find((selected) => selected.id === this.scriptId); + if (!selectedScript) { + return false; + } + return selectedScript.revert; + } + + public selectWithRevertState(newState: boolean, selection: UserSelection): void { + selection.scripts.processChanges({ + changes: [{ + scriptId: this.scriptId, + newStatus: { + isSelected: true, + isReverted: newState, + }, + }], + }); + } +} diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/ToggleSwitch.vue b/src/presentation/components/Scripts/View/Tree/NodeContent/ToggleSwitch.vue new file mode 100644 index 00000000..9b546f20 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/ToggleSwitch.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue b/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue new file mode 100644 index 00000000..c082c829 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent.ts new file mode 100644 index 00000000..52006875 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent.ts @@ -0,0 +1,34 @@ +import type { ReadOnlyTreeNode } from '../Node/TreeNode'; + +type TreeViewFilterTriggeredEvent = { + readonly action: TreeViewFilterAction.Triggered; + readonly predicate: TreeViewFilterPredicate; +}; + +type TreeViewFilterRemovedEvent = { + readonly action: TreeViewFilterAction.Removed; +}; + +export type TreeViewFilterEvent = TreeViewFilterTriggeredEvent | TreeViewFilterRemovedEvent; + +export enum TreeViewFilterAction { + Triggered, + Removed, +} + +export type TreeViewFilterPredicate = (node: ReadOnlyTreeNode) => boolean; + +export function createFilterTriggeredEvent( + predicate: TreeViewFilterPredicate, +): TreeViewFilterTriggeredEvent { + return { + action: TreeViewFilterAction.Triggered, + predicate, + }; +} + +export function createFilterRemovedEvent(): TreeViewFilterRemovedEvent { + return { + action: TreeViewFilterAction.Removed, + }; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData.ts new file mode 100644 index 00000000..09868d08 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData.ts @@ -0,0 +1,8 @@ +export type TreeInputNodeDataId = string; + +export interface TreeInputNodeData { + readonly id: TreeInputNodeDataId; + readonly children?: readonly TreeInputNodeData[]; + readonly parent?: TreeInputNodeData | null; + readonly data?: object; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeNodeStateChangedEmittedEvent.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeNodeStateChangedEmittedEvent.ts new file mode 100644 index 00000000..86a2e1c7 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeNodeStateChangedEmittedEvent.ts @@ -0,0 +1,8 @@ +import type { TreeNodeStateDescriptor } from '../Node/State/StateDescriptor'; +import type { ReadOnlyTreeNode } from '../Node/TreeNode'; + +export interface TreeNodeStateChangedEmittedEvent { + readonly node: ReadOnlyTreeNode; + readonly oldState?: TreeNodeStateDescriptor; + readonly newState: TreeNodeStateDescriptor; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/HierarchicalTreeNode.vue b/src/presentation/components/Scripts/View/Tree/TreeView/Node/HierarchicalTreeNode.vue new file mode 100644 index 00000000..5d04c734 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/HierarchicalTreeNode.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/HierarchyAccess.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/HierarchyAccess.ts new file mode 100644 index 00000000..b935795a --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/HierarchyAccess.ts @@ -0,0 +1,19 @@ +import type { ReadOnlyTreeNode, TreeNode } from '../TreeNode'; + +export interface HierarchyReader { + readonly depthInTree: number; + readonly parent: ReadOnlyTreeNode | undefined; + readonly children: readonly ReadOnlyTreeNode[]; + readonly isLeafNode: boolean; + readonly isBranchNode: boolean; +} + +export interface HierarchyWriter { + setParent(parent: TreeNode): void; + setChildren(children: readonly TreeNode[]): void; +} + +export interface HierarchyAccess extends HierarchyReader, HierarchyWriter { + readonly parent: TreeNode | undefined; + readonly children: readonly TreeNode[]; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy.ts new file mode 100644 index 00000000..c15fe48a --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy.ts @@ -0,0 +1,31 @@ +import type { TreeNode } from '../TreeNode'; +import type { HierarchyAccess } from './HierarchyAccess'; + +export class TreeNodeHierarchy implements HierarchyAccess { + public parent: TreeNode | undefined = undefined; + + public get depthInTree(): number { + if (!this.parent) { + return 0; + } + return this.parent.hierarchy.depthInTree + 1; + } + + public get isLeafNode(): boolean { + return this.children.length === 0; + } + + public get isBranchNode(): boolean { + return this.children.length > 0; + } + + public children: readonly TreeNode[]; + + public setChildren(children: readonly TreeNode[]): void { + this.children = children; + } + + public setParent(parent: TreeNode | undefined): void { + this.parent = parent; + } +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/InteractableNode.vue b/src/presentation/components/Scripts/View/Tree/TreeView/Node/InteractableNode.vue new file mode 100644 index 00000000..94b052a2 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/InteractableNode.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/LeafTreeNode.vue b/src/presentation/components/Scripts/View/Tree/TreeView/Node/LeafTreeNode.vue new file mode 100644 index 00000000..db17945b --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/LeafTreeNode.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/NodeCheckbox.vue b/src/presentation/components/Scripts/View/Tree/TreeView/Node/NodeCheckbox.vue new file mode 100644 index 00000000..df447660 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/NodeCheckbox.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState.ts new file mode 100644 index 00000000..048437b4 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState.ts @@ -0,0 +1,5 @@ +export enum TreeNodeCheckState { + Unchecked = 0, + Checked = 1, + Indeterminate = 2, +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess.ts new file mode 100644 index 00000000..3d9671dc --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess.ts @@ -0,0 +1,43 @@ +import type { IEventSource } from '@/infrastructure/Events/IEventSource'; +import { TreeNodeCheckState } from './CheckState'; +import type { TreeNodeStateDescriptor } from './StateDescriptor'; + +export interface NodeStateChangedEvent { + readonly oldState: TreeNodeStateDescriptor; + readonly newState: TreeNodeStateDescriptor; +} + +export interface TreeNodeStateReader { + readonly current: TreeNodeStateDescriptor; + readonly changed: IEventSource; +} + +/* + The transactional approach allows for batched state changes. + Instead of firing a state change event for every single operation, + multiple changes can be batched into a single transaction. + This ensures that listeners to the state change event are + only notified once per batch of changes, optimizing performance + and reducing potential event handling overhead. +*/ +export interface TreeNodeStateTransactor { + beginTransaction(): TreeNodeStateTransaction; + commitTransaction(transaction: TreeNodeStateTransaction): void; +} + +export interface TreeNodeStateTransaction { + withExpansionState(isExpanded: boolean): TreeNodeStateTransaction; + withMatchState(isMatched: boolean): TreeNodeStateTransaction; + withFocusState(isFocused: boolean): TreeNodeStateTransaction; + withVisibilityState(isVisible: boolean): TreeNodeStateTransaction; + withCheckState(checkState: TreeNodeCheckState): TreeNodeStateTransaction; + readonly updatedState: Partial; +} + +export interface TreeNodeStateWriter extends TreeNodeStateTransactor { + toggleCheck(): void; + toggleExpand(): void; +} + +export interface TreeNodeStateAccess + extends TreeNodeStateReader, TreeNodeStateWriter { } diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor.ts new file mode 100644 index 00000000..59fb4c5e --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor.ts @@ -0,0 +1,9 @@ +import { TreeNodeCheckState } from './CheckState'; + +export interface TreeNodeStateDescriptor { + readonly checkState: TreeNodeCheckState; + readonly isExpanded: boolean; + readonly isVisible: boolean; + readonly isMatched: boolean; + readonly isFocused: boolean; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeState.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeState.ts new file mode 100644 index 00000000..f7b48c3c --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeState.ts @@ -0,0 +1,66 @@ +import { EventSource } from '@/infrastructure/Events/EventSource'; +import { TreeNodeStateTransactionDescriber } from './TreeNodeStateTransactionDescriber'; +import { TreeNodeCheckState } from './CheckState'; +import type { NodeStateChangedEvent, TreeNodeStateAccess, TreeNodeStateTransaction } from './StateAccess'; +import type { TreeNodeStateDescriptor } from './StateDescriptor'; + +export class TreeNodeState implements TreeNodeStateAccess { + public current: TreeNodeStateDescriptor = { + checkState: TreeNodeCheckState.Unchecked, + isExpanded: false, + isVisible: true, + isMatched: false, + isFocused: false, + }; + + public readonly changed = new EventSource(); + + public beginTransaction(): TreeNodeStateTransaction { + return new TreeNodeStateTransactionDescriber(); + } + + public commitTransaction(transaction: TreeNodeStateTransaction): void { + const oldState = this.current; + const newState: TreeNodeStateDescriptor = { + ...this.current, + ...transaction.updatedState, + }; + if (areEqual(oldState, newState)) { + return; + } + this.current = newState; + const event: NodeStateChangedEvent = { + oldState, + newState, + }; + this.changed.notify(event); + } + + public toggleCheck(): void { + const checkStateTransitions: { + readonly [K in TreeNodeCheckState]: TreeNodeCheckState; + } = { + [TreeNodeCheckState.Checked]: TreeNodeCheckState.Unchecked, + [TreeNodeCheckState.Unchecked]: TreeNodeCheckState.Checked, + [TreeNodeCheckState.Indeterminate]: TreeNodeCheckState.Unchecked, + }; + + this.commitTransaction( + this.beginTransaction().withCheckState(checkStateTransitions[this.current.checkState]), + ); + } + + public toggleExpand(): void { + this.commitTransaction( + this.beginTransaction().withExpansionState(!this.current.isExpanded), + ); + } +} + +function areEqual(first: TreeNodeStateDescriptor, second: TreeNodeStateDescriptor): boolean { + return first.isFocused === second.isFocused + && first.isMatched === second.isMatched + && first.isVisible === second.isVisible + && first.isExpanded === second.isExpanded + && first.checkState === second.checkState; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeStateTransactionDescriber.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeStateTransactionDescriber.ts new file mode 100644 index 00000000..0c34ac18 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeStateTransactionDescriber.ts @@ -0,0 +1,44 @@ +import { TreeNodeCheckState } from './CheckState'; +import type { TreeNodeStateTransaction } from './StateAccess'; +import type { TreeNodeStateDescriptor } from './StateDescriptor'; + +export class TreeNodeStateTransactionDescriber implements TreeNodeStateTransaction { + constructor(public updatedState: Partial = {}) { } + + public withExpansionState(isExpanded: boolean): TreeNodeStateTransaction { + return this.describeChange({ + isExpanded, + }); + } + + public withMatchState(isMatched: boolean): TreeNodeStateTransaction { + return this.describeChange({ + isMatched, + }); + } + + public withFocusState(isFocused: boolean): TreeNodeStateTransaction { + return this.describeChange({ + isFocused, + }); + } + + public withVisibilityState(isVisible: boolean): TreeNodeStateTransaction { + return this.describeChange({ + isVisible, + }); + } + + public withCheckState(checkState: TreeNodeCheckState): TreeNodeStateTransaction { + return this.describeChange({ + checkState, + }); + } + + private describeChange(changedState: Partial): TreeNodeStateTransaction { + return new TreeNodeStateTransactionDescriber({ + ...this.updatedState, + ...changedState, + }); + } +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode.ts new file mode 100644 index 00000000..7e4f8f1a --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode.ts @@ -0,0 +1,16 @@ +import type { HierarchyAccess, HierarchyReader } from './Hierarchy/HierarchyAccess'; +import type { TreeNodeStateAccess, TreeNodeStateReader } from './State/StateAccess'; + +export type TreeNodeId = string; + +export interface ReadOnlyTreeNode { + readonly id: TreeNodeId; + readonly state: TreeNodeStateReader; + readonly hierarchy: HierarchyReader; + readonly metadata?: object; +} + +export interface TreeNode extends ReadOnlyTreeNode { + readonly state: TreeNodeStateAccess; + readonly hierarchy: HierarchyAccess; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNodeManager.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNodeManager.ts new file mode 100644 index 00000000..f0c5669d --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNodeManager.ts @@ -0,0 +1,21 @@ +import { TreeNodeHierarchy } from './Hierarchy/TreeNodeHierarchy'; +import { TreeNodeState } from './State/TreeNodeState'; +import type { TreeNode, TreeNodeId } from './TreeNode'; +import type { TreeNodeStateAccess } from './State/StateAccess'; +import type { HierarchyAccess } from './Hierarchy/HierarchyAccess'; + +export class TreeNodeManager implements TreeNode { + public readonly state: TreeNodeStateAccess; + + public readonly hierarchy: HierarchyAccess; + + constructor(public readonly id: TreeNodeId, public readonly metadata?: object) { + if (!id) { + throw new Error('missing id'); + } + + this.hierarchy = new TreeNodeHierarchy(); + + this.state = new TreeNodeState(); + } +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState.ts new file mode 100644 index 00000000..5ab688fa --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState.ts @@ -0,0 +1,31 @@ +import { ref } from 'vue'; +import { useAutoUnsubscribedEventListener, type UseEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener'; + +export function useKeyboardInteractionState( + eventTarget: EventTarget = DefaultEventSource, + useEventListener: UseEventListener = useAutoUnsubscribedEventListener, +) { + const { startListening } = useEventListener(); + const isKeyboardBeingUsed = ref(false); + + const enableKeyboardFocus = () => { + if (isKeyboardBeingUsed.value) { + return; + } + isKeyboardBeingUsed.value = true; + }; + + const disableKeyboardFocus = () => { + if (!isKeyboardBeingUsed.value) { + return; + } + isKeyboardBeingUsed.value = false; + }; + + startListening(eventTarget, 'keydown', enableKeyboardFocus); + startListening(eventTarget, 'click', disableKeyboardFocus); + + return { isKeyboardBeingUsed }; +} + +export const DefaultEventSource = document; diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Node/UseNodeState.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Node/UseNodeState.ts new file mode 100644 index 00000000..36de73c8 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Node/UseNodeState.ts @@ -0,0 +1,27 @@ +import { + type Ref, shallowRef, watch, shallowReadonly, +} from 'vue'; +import { injectKey } from '@/presentation/injectionSymbols'; +import type { ReadOnlyTreeNode } from './TreeNode'; +import type { TreeNodeStateDescriptor } from './State/StateDescriptor'; + +export function useNodeState( + nodeRef: Readonly>, +) { + const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents); + + const state = shallowRef(nodeRef.value.state.current); + + watch(nodeRef, (node: ReadOnlyTreeNode) => { + state.value = node.state.current; + events.unsubscribeAllAndRegister([ + node.state.changed.on((change) => { + state.value = change.newState; + }), + ]); + }, { immediate: true }); + + return { + state: shallowReadonly(state), + }; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/DelayScheduler.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/DelayScheduler.ts new file mode 100644 index 00000000..4aac73b7 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/DelayScheduler.ts @@ -0,0 +1,3 @@ +export interface DelayScheduler { + scheduleNext(callback: () => void, delayInMs: number): void; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts new file mode 100644 index 00000000..6ac0bae6 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts @@ -0,0 +1,35 @@ +import type { ReadOnlyTreeNode } from '../../Node/TreeNode'; +import type { RenderQueueOrderer } from './RenderQueueOrderer'; + +export class CollapsedParentOrderer implements RenderQueueOrderer { + public orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { + return orderNodes(nodes); + } +} + +function orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { + return [...nodes] + .map((node, index) => ({ node, index })) + .sort((a, b) => { + const [ + isANodeOfCollapsedParent, + isBNodeOfCollapsedParent, + ] = [isParentCollapsed(a.node), isParentCollapsed(b.node)]; + if (isANodeOfCollapsedParent !== isBNodeOfCollapsedParent) { + return (isANodeOfCollapsedParent ? 1 : 0) - (isBNodeOfCollapsedParent ? 1 : 0); + } + return a.index - b.index; + }) + .map(({ node }) => node); +} + +function isParentCollapsed(node: ReadOnlyTreeNode): boolean { + const parentNode = node.hierarchy.parent; + if (parentNode) { + if (!parentNode.state.current.isExpanded) { + return true; + } + return isParentCollapsed(parentNode); + } + return false; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/RenderQueueOrderer.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/RenderQueueOrderer.ts new file mode 100644 index 00000000..beafa3e5 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/RenderQueueOrderer.ts @@ -0,0 +1,5 @@ +import type { ReadOnlyTreeNode } from '../../Node/TreeNode'; + +export interface RenderQueueOrderer { + orderNodes(nodes: Iterable): ReadOnlyTreeNode[]; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/NodeRenderingStrategy.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/NodeRenderingStrategy.ts new file mode 100644 index 00000000..eac07508 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/NodeRenderingStrategy.ts @@ -0,0 +1,5 @@ +import type { TreeNode } from '../../Node/TreeNode'; + +export interface NodeRenderingStrategy { + shouldRender(node: TreeNode): boolean; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/TimeoutDelayScheduler.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/TimeoutDelayScheduler.ts new file mode 100644 index 00000000..e79f4653 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/TimeoutDelayScheduler.ts @@ -0,0 +1,28 @@ +import type { DelayScheduler } from '../DelayScheduler'; + +export class TimeoutDelayScheduler implements DelayScheduler { + private timeoutId: ReturnType | undefined = undefined; + + constructor(private readonly timer: TimeFunctions = { + clearTimeout: globalThis.clearTimeout.bind(globalThis), + setTimeout: globalThis.setTimeout.bind(globalThis), + }) { } + + public scheduleNext(callback: () => void, delayInMs: number): void { + this.clear(); + this.timeoutId = this.timer.setTimeout(callback, delayInMs); + } + + private clear(): void { + if (this.timeoutId === undefined) { + return; + } + this.timer.clearTimeout(this.timeoutId); + this.timeoutId = undefined; + } +} + +export interface TimeFunctions { + clearTimeout(id: ReturnType): void; + setTimeout(callback: () => void, delayInMs: number): ReturnType; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts new file mode 100644 index 00000000..8b6c6064 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts @@ -0,0 +1,131 @@ +import { + type Ref, shallowRef, triggerRef, watch, +} from 'vue'; +import { useNodeStateChangeAggregator } from '../UseNodeStateChangeAggregator'; +import { useCurrentTreeNodes } from '../UseCurrentTreeNodes'; +import { TimeoutDelayScheduler } from './Scheduling/TimeoutDelayScheduler'; +import { CollapsedParentOrderer } from './Ordering/CollapsedParentOrderer'; +import type { ReadOnlyTreeNode } from '../Node/TreeNode'; +import type { TreeRoot } from '../TreeRoot/TreeRoot'; +import type { QueryableNodes } from '../TreeRoot/NodeCollection/Query/QueryableNodes'; +import type { NodeRenderingStrategy } from './Scheduling/NodeRenderingStrategy'; +import type { DelayScheduler } from './DelayScheduler'; +import type { RenderQueueOrderer } from './Ordering/RenderQueueOrderer'; + +export interface NodeRenderingControl { + readonly renderingStrategy: NodeRenderingStrategy; + clearRenderingStates(): void; + notifyRenderingUpdates(): void; +} + +/** + * Renders tree nodes gradually to prevent UI freeze when loading large amounts of nodes. + */ +export function useGradualNodeRendering( + treeRootRef: Readonly>, + useChangeAggregator = useNodeStateChangeAggregator, + useTreeNodes = useCurrentTreeNodes, + scheduler: DelayScheduler = new TimeoutDelayScheduler(), + initialBatchSize = 30, + subsequentBatchSize = 5, + orderer: RenderQueueOrderer = new CollapsedParentOrderer(), +): NodeRenderingControl { + const nodesToRender = new Set(); + const nodesBeingRendered = shallowRef(new Set()); + let isRenderingInProgress = false; + const renderingDelayInMs = 50; + + const { onNodeStateChange } = useChangeAggregator(treeRootRef); + const { nodes } = useTreeNodes(treeRootRef); + + function notifyRenderingUpdates() { + triggerRef(nodesBeingRendered); + } + + function updateNodeRenderQueue(node: ReadOnlyTreeNode, isVisible: boolean) { + if (isVisible + && !nodesToRender.has(node) + && !nodesBeingRendered.value.has(node)) { + nodesToRender.add(node); + beginRendering(); + } else if (!isVisible) { + if (nodesToRender.has(node)) { + nodesToRender.delete(node); + } + if (nodesBeingRendered.value.has(node)) { + nodesBeingRendered.value.delete(node); + notifyRenderingUpdates(); + } + } + } + + function clearRenderingStates() { + nodesToRender.clear(); + nodesBeingRendered.value.clear(); + } + + function initializeAndRenderNodes(newNodes: QueryableNodes) { + clearRenderingStates(); + if (!newNodes || newNodes.flattenedNodes.length === 0) { + notifyRenderingUpdates(); + return; + } + newNodes + .flattenedNodes + .filter((node) => node.state.current.isVisible) + .forEach((node) => nodesToRender.add(node)); + beginRendering(); + } + + watch(nodes, (newNodes) => { + initializeAndRenderNodes(newNodes); + }, { immediate: true }); + + onNodeStateChange((change) => { + if (change.newState.isVisible === change.oldState?.isVisible) { + return; + } + updateNodeRenderQueue(change.node, change.newState.isVisible); + }); + + function beginRendering() { + if (isRenderingInProgress) { + return; + } + renderNextBatch(initialBatchSize); + } + + function renderNextBatch(batchSize: number) { + if (nodesToRender.size === 0) { + isRenderingInProgress = false; + return; + } + isRenderingInProgress = true; + const orderedNodes = orderer.orderNodes(nodesToRender); + const currentBatch = orderedNodes.slice(0, batchSize); + if (currentBatch.length === 0) { + return; + } + currentBatch.forEach((node) => { + nodesToRender.delete(node); + nodesBeingRendered.value.add(node); + }); + notifyRenderingUpdates(); + scheduler.scheduleNext( + () => renderNextBatch(subsequentBatchSize), + renderingDelayInMs, + ); + } + + function shouldNodeBeRendered(node: ReadOnlyTreeNode): boolean { + return nodesBeingRendered.value.has(node); + } + + return { + renderingStrategy: { + shouldRender: shouldNodeBeRendered, + }, + clearRenderingStates, + notifyRenderingUpdates, + }; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeCollectionFocusManager.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeCollectionFocusManager.ts new file mode 100644 index 00000000..4159214e --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeCollectionFocusManager.ts @@ -0,0 +1,21 @@ +import type { ReadOnlyTreeNode, TreeNode } from '../../Node/TreeNode'; +import type { TreeNodeCollection } from '../NodeCollection/TreeNodeCollection'; +import type { SingleNodeFocusManager } from './SingleNodeFocusManager'; + +export class SingleNodeCollectionFocusManager implements SingleNodeFocusManager { + public get currentSingleFocusedNode(): TreeNode | undefined { + const focusedNodes = this.collection.nodes.flattenedNodes.filter( + (node) => node.state.current.isFocused, + ); + return focusedNodes.length === 1 ? focusedNodes[0] : undefined; + } + + public setSingleFocus(focusedNode: ReadOnlyTreeNode): void { + this.collection.nodes.flattenedNodes.forEach((node) => { + const isFocused = node === focusedNode; + node.state.commitTransaction(node.state.beginTransaction().withFocusState(isFocused)); + }); + } + + constructor(private readonly collection: TreeNodeCollection) { } +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeFocusManager.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeFocusManager.ts new file mode 100644 index 00000000..47bad8f5 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeFocusManager.ts @@ -0,0 +1,6 @@ +import type { ReadOnlyTreeNode, TreeNode } from '../../Node/TreeNode'; + +export interface SingleNodeFocusManager { + readonly currentSingleFocusedNode: TreeNode | undefined; + setSingleFocus(focusedNode: ReadOnlyTreeNode): void; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes.ts new file mode 100644 index 00000000..e6336d30 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes.ts @@ -0,0 +1,15 @@ +import type { ReadOnlyTreeNode, TreeNode, TreeNodeId } from '../../../Node/TreeNode'; + +export interface ReadOnlyQueryableNodes { + readonly rootNodes: readonly ReadOnlyTreeNode[]; + readonly flattenedNodes: readonly ReadOnlyTreeNode[]; + + getNodeById(nodeId: TreeNodeId): ReadOnlyTreeNode; +} + +export interface QueryableNodes extends ReadOnlyQueryableNodes { + readonly rootNodes: readonly TreeNode[]; + readonly flattenedNodes: readonly TreeNode[]; + + getNodeById(nodeId: TreeNodeId): TreeNode; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/TreeNodeNavigator.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/TreeNodeNavigator.ts new file mode 100644 index 00000000..f27c68a3 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/TreeNodeNavigator.ts @@ -0,0 +1,28 @@ +import type { QueryableNodes } from './QueryableNodes'; +import type { TreeNode, TreeNodeId } from '../../../Node/TreeNode'; + +export class TreeNodeNavigator implements QueryableNodes { + public readonly flattenedNodes: readonly TreeNode[]; + + constructor(public readonly rootNodes: readonly TreeNode[]) { + this.flattenedNodes = flattenNodes(rootNodes); + } + + public getNodeById(nodeId: TreeNodeId): TreeNode { + const foundNode = this.flattenedNodes.find((node) => node.id === nodeId); + if (!foundNode) { + throw new Error(`Node could not be found: ${nodeId}`); + } + return foundNode; + } +} + +function flattenNodes(nodes: readonly TreeNode[]): TreeNode[] { + return nodes.reduce((flattenedNodes, node) => { + flattenedNodes.push(node); + if (node.hierarchy.children) { + flattenedNodes.push(...flattenNodes(node.hierarchy.children)); + } + return flattenedNodes; + }, new Array()); +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts new file mode 100644 index 00000000..72949093 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts @@ -0,0 +1,24 @@ +import { isArray } from '@/TypeHelpers'; +import { TreeNodeManager } from '../../Node/TreeNodeManager'; +import type { TreeInputNodeData } from '../../Bindings/TreeInputNodeData'; +import type { TreeNode } from '../../Node/TreeNode'; + +export function parseTreeInput( + input: readonly TreeInputNodeData[], +): TreeNode[] { + if (!isArray(input)) { + throw new Error('input data must be an array'); + } + const nodes = input.map((nodeData) => createNode(nodeData)); + return nodes; +} + +function createNode(input: TreeInputNodeData): TreeNode { + const node = new TreeNodeManager(input.id, input.data); + node.hierarchy.setChildren(input.children?.map((child) => { + const childNode = createNode(child); + childNode.hierarchy.setParent(node); + return childNode; + }) ?? []); + return node; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeCollection.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeCollection.ts new file mode 100644 index 00000000..98c2d93d --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeCollection.ts @@ -0,0 +1,15 @@ +import type { IEventSource } from '@/infrastructure/Events/IEventSource'; +import type { TreeInputNodeData } from '../../Bindings/TreeInputNodeData'; +import type { QueryableNodes, ReadOnlyQueryableNodes } from './Query/QueryableNodes'; + +export interface ReadOnlyTreeNodeCollection { + readonly nodes: ReadOnlyQueryableNodes; + readonly nodesUpdated: IEventSource; + updateRootNodes(rootNodes: readonly TreeInputNodeData[]): void; +} + +export interface TreeNodeCollection extends ReadOnlyTreeNodeCollection { + readonly nodes: QueryableNodes; + readonly nodesUpdated: IEventSource; + updateRootNodes(rootNodes: readonly TreeInputNodeData[]): void; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeInitializerAndUpdater.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeInitializerAndUpdater.ts new file mode 100644 index 00000000..c4c34df6 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeInitializerAndUpdater.ts @@ -0,0 +1,23 @@ +import { EventSource } from '@/infrastructure/Events/EventSource'; +import { TreeNodeNavigator } from './Query/TreeNodeNavigator'; +import { parseTreeInput } from './TreeInputParser'; +import type { TreeInputNodeData } from '../../Bindings/TreeInputNodeData'; +import type { TreeNodeCollection } from './TreeNodeCollection'; +import type { QueryableNodes } from './Query/QueryableNodes'; + +export class TreeNodeInitializerAndUpdater implements TreeNodeCollection { + public nodes: QueryableNodes = new TreeNodeNavigator([]); + + public nodesUpdated = new EventSource(); + + public updateRootNodes(rootNodesData: readonly TreeInputNodeData[]): void { + if (!rootNodesData.length) { + throw new Error('missing data'); + } + const rootNodes = this.treeNodeParser(rootNodesData); + this.nodes = new TreeNodeNavigator(rootNodes); + this.nodesUpdated.notify(this.nodes); + } + + constructor(private readonly treeNodeParser = parseTreeInput) { } +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot.ts new file mode 100644 index 00000000..55193832 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot.ts @@ -0,0 +1,7 @@ +import type { SingleNodeFocusManager } from './Focus/SingleNodeFocusManager'; +import type { TreeNodeCollection } from './NodeCollection/TreeNodeCollection'; + +export interface TreeRoot { + readonly collection: TreeNodeCollection; + readonly focus: SingleNodeFocusManager; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot.vue b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot.vue new file mode 100644 index 00000000..0901d059 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRootManager.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRootManager.ts new file mode 100644 index 00000000..f2d0b0e9 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRootManager.ts @@ -0,0 +1,21 @@ +import { SingleNodeCollectionFocusManager } from './Focus/SingleNodeCollectionFocusManager'; +import { TreeNodeInitializerAndUpdater } from './NodeCollection/TreeNodeInitializerAndUpdater'; +import type { TreeRoot } from './TreeRoot'; +import type { TreeNodeCollection } from './NodeCollection/TreeNodeCollection'; +import type { SingleNodeFocusManager } from './Focus/SingleNodeFocusManager'; + +export class TreeRootManager implements TreeRoot { + public readonly collection: TreeNodeCollection; + + public readonly focus: SingleNodeFocusManager; + + constructor( + collection: TreeNodeCollection = new TreeNodeInitializerAndUpdater(), + createFocusManager: ( + collection: TreeNodeCollection + ) => SingleNodeFocusManager = (nodes) => new SingleNodeCollectionFocusManager(nodes), + ) { + this.collection = collection; + this.focus = createFocusManager(this.collection); + } +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue b/src/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue new file mode 100644 index 00000000..bd19c42c --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateChildrenCheckState.ts b/src/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateChildrenCheckState.ts new file mode 100644 index 00000000..e48f9ee5 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateChildrenCheckState.ts @@ -0,0 +1,45 @@ +import { useNodeStateChangeAggregator } from './UseNodeStateChangeAggregator'; +import { TreeNodeCheckState } from './Node/State/CheckState'; +import type { TreeRoot } from './TreeRoot/TreeRoot'; +import type { HierarchyAccess } from './Node/Hierarchy/HierarchyAccess'; +import type { Ref } from 'vue'; + +export function useAutoUpdateChildrenCheckState( + treeRootRef: Readonly>, + useChangeAggregator = useNodeStateChangeAggregator, +) { + const { onNodeStateChange } = useChangeAggregator(treeRootRef); + + onNodeStateChange((change) => { + if (change.newState.checkState === change.oldState?.checkState) { + return; + } + updateChildrenCheckedState(change.node.hierarchy, change.newState.checkState); + }); +} + +function updateChildrenCheckedState( + node: HierarchyAccess, + newParentState: TreeNodeCheckState, +) { + if (node.isLeafNode) { + return; + } + if (!shouldUpdateChildren(newParentState)) { + return; + } + const { children } = node; + children.forEach((childNode) => { + if (childNode.state.current.checkState === newParentState) { + return; + } + childNode.state.commitTransaction( + childNode.state.beginTransaction().withCheckState(newParentState), + ); + }); +} + +function shouldUpdateChildren(newParentState: TreeNodeCheckState) { + return newParentState === TreeNodeCheckState.Checked + || newParentState === TreeNodeCheckState.Unchecked; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateParentCheckState.ts b/src/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateParentCheckState.ts new file mode 100644 index 00000000..7da6d684 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateParentCheckState.ts @@ -0,0 +1,48 @@ +import { useNodeStateChangeAggregator } from './UseNodeStateChangeAggregator'; +import { TreeNodeCheckState } from './Node/State/CheckState'; +import type { TreeRoot } from './TreeRoot/TreeRoot'; +import type { HierarchyAccess } from './Node/Hierarchy/HierarchyAccess'; +import type { ReadOnlyTreeNode } from './Node/TreeNode'; +import type { Ref } from 'vue'; + +export function useAutoUpdateParentCheckState( + treeRef: Readonly>, + useChangeAggregator = useNodeStateChangeAggregator, +) { + const { onNodeStateChange } = useChangeAggregator(treeRef); + + onNodeStateChange((change) => { + if (change.newState.checkState === change.oldState?.checkState) { + return; + } + updateNodeParentCheckedState(change.node.hierarchy); + }); +} + +function updateNodeParentCheckedState( + node: HierarchyAccess, +) { + const { parent } = node; + if (!parent) { + return; + } + const newState = getNewStateCheckedStateBasedOnChildren(parent); + if (newState === parent.state.current.checkState) { + return; + } + parent.state.commitTransaction( + parent.state.beginTransaction().withCheckState(newState), + ); +} + +function getNewStateCheckedStateBasedOnChildren(node: ReadOnlyTreeNode): TreeNodeCheckState { + const { children } = node.hierarchy; + const childrenStates = children.map((child) => child.state.current.checkState); + if (childrenStates.every((state) => state === TreeNodeCheckState.Unchecked)) { + return TreeNodeCheckState.Unchecked; + } + if (childrenStates.every((state) => state === TreeNodeCheckState.Checked)) { + return TreeNodeCheckState.Checked; + } + return TreeNodeCheckState.Indeterminate; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes.ts b/src/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes.ts new file mode 100644 index 00000000..ee029655 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes.ts @@ -0,0 +1,25 @@ +import { + watch, shallowReadonly, shallowRef, type Ref, +} from 'vue'; +import { injectKey } from '@/presentation/injectionSymbols'; +import type { TreeRoot } from './TreeRoot/TreeRoot'; +import type { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes'; + +export function useCurrentTreeNodes(treeRef: Readonly>) { + const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents); + + const nodes = shallowRef(treeRef.value.collection.nodes); + + watch(treeRef, (newTree) => { + nodes.value = newTree.collection.nodes; + events.unsubscribeAllAndRegister([ + newTree.collection.nodesUpdated.on((newNodes) => { + nodes.value = newNodes; + }), + ]); + }, { immediate: true }); + + return { + nodes: shallowReadonly(nodes), + }; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/UseLeafNodeCheckedStateUpdater.ts b/src/presentation/components/Scripts/View/Tree/TreeView/UseLeafNodeCheckedStateUpdater.ts new file mode 100644 index 00000000..b971d7a0 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/UseLeafNodeCheckedStateUpdater.ts @@ -0,0 +1,43 @@ +import { type Ref, watch } from 'vue'; +import { useCurrentTreeNodes } from './UseCurrentTreeNodes'; +import { TreeNodeCheckState } from './Node/State/CheckState'; +import type { TreeRoot } from './TreeRoot/TreeRoot'; +import type { TreeNode } from './Node/TreeNode'; +import type { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes'; + +export function useLeafNodeCheckedStateUpdater( + treeRootRef: Readonly>, + leafNodeIdsRef: Readonly>, +) { + const { nodes } = useCurrentTreeNodes(treeRootRef); + + watch( + [leafNodeIdsRef, nodes], + ([nodeIds, actualNodes]) => { + updateNodeSelections(actualNodes, nodeIds); + }, + { immediate: true }, + ); +} + +function updateNodeSelections( + nodes: QueryableNodes, + selectedNodeIds: readonly string[], +) { + nodes.flattenedNodes.forEach((node) => { + updateNodeSelection(node, selectedNodeIds); + }); +} + +function updateNodeSelection( + node: TreeNode, + selectedNodeIds: readonly string[], +) { + if (!node.hierarchy.isLeafNode) { + return; + } + const newState = selectedNodeIds.includes(node.id) + ? TreeNodeCheckState.Checked + : TreeNodeCheckState.Unchecked; + node.state.commitTransaction(node.state.beginTransaction().withCheckState(newState)); +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator.ts b/src/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator.ts new file mode 100644 index 00000000..fedb45d3 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator.ts @@ -0,0 +1,83 @@ +import { + watch, shallowRef, type Ref, +} from 'vue'; +import { injectKey } from '@/presentation/injectionSymbols'; +import type { IEventSubscription } from '@/infrastructure/Events/IEventSource'; +import { useCurrentTreeNodes } from './UseCurrentTreeNodes'; +import type { TreeNodeStateDescriptor } from './Node/State/StateDescriptor'; +import type { TreeRoot } from './TreeRoot/TreeRoot'; +import type { TreeNode } from './Node/TreeNode'; + +export type NodeStateChangeEventCallback = (args: NodeStateChangeEventArgs) => void; + +export function useNodeStateChangeAggregator( + treeRootRef: Readonly>, + useTreeNodes = useCurrentTreeNodes, +) { + const { nodes } = useTreeNodes(treeRootRef); + const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents); + + const onNodeChangeCallback = shallowRef(); + + watch( + [nodes, onNodeChangeCallback], + ([newNodes, callback]) => { + if (!callback) { // may not be registered yet + return; + } + if (!newNodes || newNodes.flattenedNodes.length === 0) { + events.unsubscribeAll(); + return; + } + const allNodes = newNodes.flattenedNodes; + events.unsubscribeAllAndRegister( + subscribeToNotifyOnFutureNodeChanges(allNodes, callback), + ); + notifyCurrentNodeState(allNodes, callback); + }, + ); + + function onNodeStateChange( + callback: NodeStateChangeEventCallback, + ): void { + if (!callback) { + throw new Error('missing callback'); + } + onNodeChangeCallback.value = callback; + } + + return { + onNodeStateChange, + }; +} + +export interface NodeStateChangeEventArgs { + readonly node: TreeNode; + readonly newState: TreeNodeStateDescriptor; + readonly oldState?: TreeNodeStateDescriptor; +} + +function notifyCurrentNodeState( + nodes: readonly TreeNode[], + callback: NodeStateChangeEventCallback, +) { + nodes.forEach((node) => { + callback({ + node, + newState: node.state.current, + }); + }); +} + +function subscribeToNotifyOnFutureNodeChanges( + nodes: readonly TreeNode[], + callback: NodeStateChangeEventCallback, +): IEventSubscription[] { + return nodes.map((node) => node.state.changed.on((stateChange) => { + callback({ + node, + oldState: stateChange.oldState, + newState: stateChange.newState, + }); + })); +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/UseTreeKeyboardNavigation.ts b/src/presentation/components/Scripts/View/Tree/TreeView/UseTreeKeyboardNavigation.ts new file mode 100644 index 00000000..9537e78e --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/UseTreeKeyboardNavigation.ts @@ -0,0 +1,180 @@ +import { type Ref } from 'vue'; +import { useAutoUnsubscribedEventListener, type UseEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener'; +import { TreeNodeCheckState } from './Node/State/CheckState'; +import type { TreeNode } from './Node/TreeNode'; +import type { TreeRoot } from './TreeRoot/TreeRoot'; +import type { SingleNodeFocusManager } from './TreeRoot/Focus/SingleNodeFocusManager'; +import type { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes'; + +type TreeNavigationKeyCodes = 'ArrowLeft' | 'ArrowUp' | 'ArrowRight' | 'ArrowDown' | ' ' | 'Enter'; + +export function useTreeKeyboardNavigation( + treeRootRef: Readonly>, + treeElementRef: Readonly>, + useEventListener: UseEventListener = useAutoUnsubscribedEventListener, +) { + const { startListening } = useEventListener(); + startListening(treeElementRef, 'keydown', (event) => { + if (!treeElementRef.value) { + return; // Not yet initialized? + } + + const treeRoot = treeRootRef.value; + + const keyCode = event.key as TreeNavigationKeyCodes; + + if (!treeRoot.focus.currentSingleFocusedNode) { + return; + } + + const action = KeyToActionMapping[keyCode]; + + if (!action) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + action({ + focus: treeRoot.focus, + nodes: treeRoot.collection.nodes, + }); + }); +} + +interface TreeNavigationContext { + readonly focus: SingleNodeFocusManager; + readonly nodes: QueryableNodes; +} + +const KeyToActionMapping: Record< +TreeNavigationKeyCodes, +(context: TreeNavigationContext) => void +> = { + ArrowLeft: collapseNodeOrFocusParent, + ArrowUp: focusPreviousVisibleNode, + ArrowRight: expandNodeOrFocusFirstChild, + ArrowDown: focusNextVisibleNode, + ' ': toggleTreeNodeCheckStatus, + Enter: toggleTreeNodeCheckStatus, +}; + +function focusPreviousVisibleNode(context: TreeNavigationContext): void { + const focusedNode = context.focus.currentSingleFocusedNode; + if (!focusedNode) { + return; + } + const previousVisibleNode = findPreviousVisibleNode( + focusedNode, + context.nodes, + ); + if (!previousVisibleNode) { + return; + } + context.focus.setSingleFocus(previousVisibleNode); +} + +function focusNextVisibleNode(context: TreeNavigationContext): void { + const focusedNode = context.focus.currentSingleFocusedNode; + if (!focusedNode) { + return; + } + const nextVisibleNode = findNextVisibleNode(focusedNode, context.nodes); + if (!nextVisibleNode) { + return; + } + context.focus.setSingleFocus(nextVisibleNode); +} + +function toggleTreeNodeCheckStatus(context: TreeNavigationContext): void { + const focusedNode = context.focus.currentSingleFocusedNode; + if (!focusedNode) { + return; + } + const nodeState = focusedNode.state; + let transaction = nodeState.beginTransaction(); + if (nodeState.current.checkState === TreeNodeCheckState.Checked) { + transaction = transaction.withCheckState(TreeNodeCheckState.Unchecked); + } else { + transaction = transaction.withCheckState(TreeNodeCheckState.Checked); + } + nodeState.commitTransaction(transaction); +} + +function collapseNodeOrFocusParent(context: TreeNavigationContext): void { + const focusedNode = context.focus.currentSingleFocusedNode; + if (!focusedNode) { + return; + } + const nodeState = focusedNode.state; + if (focusedNode.hierarchy.isBranchNode && nodeState.current.isExpanded) { + nodeState.commitTransaction( + nodeState.beginTransaction().withExpansionState(false), + ); + } else { + const parentNode = focusedNode.hierarchy.parent; + if (!parentNode) { + return; + } + context.focus.setSingleFocus(parentNode); + } +} + +function expandNodeOrFocusFirstChild(context: TreeNavigationContext): void { + const focusedNode = context.focus.currentSingleFocusedNode; + if (!focusedNode) { + return; + } + const nodeState = focusedNode.state; + if (focusedNode.hierarchy.isBranchNode && !nodeState.current.isExpanded) { + nodeState.commitTransaction( + nodeState.beginTransaction().withExpansionState(true), + ); + return; + } + if (focusedNode.hierarchy.children.length === 0) { + return; + } + const firstChildNode = focusedNode.hierarchy.children[0]; + if (firstChildNode) { + context.focus.setSingleFocus(firstChildNode); + } +} + +function findNextVisibleNode(node: TreeNode, nodes: QueryableNodes): TreeNode | undefined { + if (node.hierarchy.children.length && node.state.current.isExpanded) { + return node.hierarchy.children[0]; + } + const nextNode = findNextNode(node, nodes); + const parentNode = node.hierarchy.parent; + if (!nextNode && parentNode) { + const nextSibling = findNextNode(parentNode, nodes); + return nextSibling; + } + return nextNode; +} + +function findNextNode(node: TreeNode, nodes: QueryableNodes): TreeNode | undefined { + const index = nodes.flattenedNodes.indexOf(node); + return nodes.flattenedNodes[index + 1] || undefined; +} + +function findPreviousVisibleNode( + node: TreeNode, + nodes: QueryableNodes, +): TreeNode | undefined { + const previousNode = findPreviousNode(node, nodes); + if (!previousNode) { + return node.hierarchy.parent; + } + if (previousNode.hierarchy.children.length && previousNode.state.current.isExpanded) { + return previousNode.hierarchy.children[previousNode.hierarchy.children.length - 1]; + } + return previousNode; +} + +function findPreviousNode(node: TreeNode, nodes: QueryableNodes): TreeNode | undefined { + const index = nodes.flattenedNodes.indexOf(node); + return nodes.flattenedNodes[index - 1] || undefined; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/UseTreeQueryFilter.ts b/src/presentation/components/Scripts/View/Tree/TreeView/UseTreeQueryFilter.ts new file mode 100644 index 00000000..fef6d3bd --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/UseTreeQueryFilter.ts @@ -0,0 +1,202 @@ +import { type Ref, watch } from 'vue'; +import { TreeViewFilterAction, type TreeViewFilterEvent, type TreeViewFilterPredicate } from './Bindings/TreeInputFilterEvent'; +import { useCurrentTreeNodes } from './UseCurrentTreeNodes'; +import type { QueryableNodes, ReadOnlyQueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes'; +import type { TreeNodeStateTransaction } from './Node/State/StateAccess'; +import type { TreeNodeStateDescriptor } from './Node/State/StateDescriptor'; +import type { ReadOnlyTreeNode, TreeNode } from './Node/TreeNode'; +import type { TreeRoot } from './TreeRoot/TreeRoot'; + +export function useTreeQueryFilter( + latestFilterEventRef: Readonly>, + treeRootRef: Readonly>, +) { + const { nodes } = useCurrentTreeNodes(treeRootRef); + + let isFiltering = false; + + const statesBeforeFiltering = new NodeStateRestorer(); + statesBeforeFiltering.saveStateBeforeFilter(nodes.value); + + setupWatchers({ + filterEventRef: latestFilterEventRef, + nodesRef: nodes, + onFilterTrigger: (predicate, newNodes) => runFilter( + newNodes, + predicate, + ), + onFilterReset: () => resetFilter(nodes.value), + }); + + function resetFilter(currentNodes: QueryableNodes) { + if (!isFiltering) { + return; + } + isFiltering = false; + currentNodes.flattenedNodes.forEach((node: TreeNode) => { + let transaction = node.state.beginTransaction() + .withMatchState(false); + transaction = statesBeforeFiltering.applyStateBeforeFilter(node, transaction); + node.state.commitTransaction(transaction); + }); + statesBeforeFiltering.clear(); + } + + function runFilter(currentNodes: QueryableNodes, predicate: TreeViewFilterPredicate) { + if (!isFiltering) { + statesBeforeFiltering.saveStateBeforeFilter(currentNodes); + isFiltering = true; + } + const { matchedNodes, unmatchedNodes } = partitionNodesByMatchCriteria(currentNodes, predicate); + const nodeTransactions = getNodeChangeTransactions(matchedNodes, unmatchedNodes); + + nodeTransactions.forEach((transaction, node) => { + node.state.commitTransaction(transaction); + }); + } +} + +function getNodeChangeTransactions( + matchedNodes: Iterable, + unmatchedNodes: Iterable, +) { + const transactions = new Map(); + + for (const unmatchedNode of unmatchedNodes) { + addOrUpdateTransaction(unmatchedNode, (builder) => builder + .withVisibilityState(false) + .withMatchState(false)); + } + + for (const matchedNode of matchedNodes) { + addOrUpdateTransaction(matchedNode, (builder) => { + let transaction = builder + .withVisibilityState(true) + .withMatchState(true); + if (matchedNode.hierarchy.isBranchNode) { + transaction = transaction.withExpansionState(false); + } + return transaction; + }); + + traverseAllChildren(matchedNode, (childNode) => { + addOrUpdateTransaction(childNode, (builder) => builder + .withVisibilityState(true)); + }); + + traverseAllParents(matchedNode, (parentNode) => { + addOrUpdateTransaction(parentNode, (builder) => builder + .withVisibilityState(true) + .withExpansionState(true)); + }); + } + + function addOrUpdateTransaction( + node: TreeNode, + builder: (transaction: TreeNodeStateTransaction) => TreeNodeStateTransaction, + ) { + let transaction = transactions.get(node) ?? node.state.beginTransaction(); + transaction = builder(transaction); + transactions.set(node, transaction); + } + + return transactions; +} + +function partitionNodesByMatchCriteria( + currentNodes: QueryableNodes, + predicate: TreeViewFilterPredicate, +) { + const matchedNodes = new Set(); + const unmatchedNodes = new Set(); + currentNodes.flattenedNodes.forEach((node) => { + if (predicate(node)) { + matchedNodes.add(node); + } else { + unmatchedNodes.add(node); + } + }); + return { + matchedNodes, + unmatchedNodes, + }; +} + +function traverseAllParents(node: TreeNode, handler: (node: TreeNode) => void) { + const parentNode = node.hierarchy.parent; + if (parentNode) { + handler(parentNode); + traverseAllParents(parentNode, handler); + } +} + +function traverseAllChildren(node: TreeNode, handler: (node: TreeNode) => void) { + node.hierarchy.children.forEach((childNode) => { + handler(childNode); + traverseAllChildren(childNode, handler); + }); +} + +class NodeStateRestorer { + private readonly originalStates = new Map>(); + + public saveStateBeforeFilter(nodes: ReadOnlyQueryableNodes) { + nodes + .flattenedNodes + .forEach((node) => { + this.originalStates.set(node, { + isExpanded: node.state.current.isExpanded, + isVisible: node.state.current.isVisible, + }); + }); + } + + public applyStateBeforeFilter( + node: TreeNode, + transaction: TreeNodeStateTransaction, + ): TreeNodeStateTransaction { + const originalState = this.originalStates.get(node); + if (!originalState) { + return transaction; + } + if (originalState.isExpanded !== undefined) { + transaction = transaction.withExpansionState(originalState.isExpanded); + } + if (originalState.isVisible !== undefined) { + transaction = transaction.withVisibilityState(originalState.isVisible); + } + return transaction; + } + + public clear() { + this.originalStates.clear(); + } +} + +function setupWatchers(options: { + readonly filterEventRef: Readonly>, + readonly nodesRef: Readonly>, + readonly onFilterReset: () => void, + readonly onFilterTrigger: ( + predicate: TreeViewFilterPredicate, + nodes: QueryableNodes, + ) => void, +}) { + watch( + [ + options.filterEventRef, + options.nodesRef, + ], + ([filterEvent, nodes]) => { + if (filterEvent === undefined) { + return; + } + if (filterEvent.action === TreeViewFilterAction.Triggered) { + options.onFilterTrigger(filterEvent.predicate, nodes); + } else { + options.onFilterReset(); + } + }, + { immediate: true }, + ); +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/tree-colors.scss b/src/presentation/components/Scripts/View/Tree/TreeView/tree-colors.scss new file mode 100644 index 00000000..5ecfa73e --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/tree-colors.scss @@ -0,0 +1,13 @@ +@use "@/presentation/assets/styles/main" as *; + +/* Tree colors, based on global colors */ +$color-tree-bg : $color-scripts-bg; +$color-node-arrow : $color-on-primary; +$color-node-fg : $color-on-primary; +$color-node-highlight-bg : $color-primary-dark; +$color-node-checkbox-bg-checked : $color-secondary; +$color-node-checkbox-bg-unchecked : $color-primary-darkest; +$color-node-checkbox-border-checked : $color-secondary; +$color-node-checkbox-border-unchecked : $color-on-primary; +$color-node-checkbox-border-indeterminate : $color-on-primary; +$color-node-checkbox-tick-checked : $color-on-secondary; diff --git a/src/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter.ts b/src/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter.ts new file mode 100644 index 00000000..3d3eec31 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter.ts @@ -0,0 +1,73 @@ +import type { Category } from '@/domain/Executables/Category/Category'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { Script } from '@/domain/Executables/Script/Script'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; +import type { Executable } from '@/domain/Executables/Executable'; +import { type NodeMetadata, NodeType } from '../NodeContent/NodeMetadata'; +import type { TreeNodeId } from '../TreeView/Node/TreeNode'; + +export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] { + return createCategoryNodes(collection.actions); +} + +export function parseSingleCategory( + categoryId: ExecutableId, + collection: ICategoryCollection, +): NodeMetadata[] { + const category = collection.getCategory(categoryId); + const tree = parseCategoryRecursively(category); + return tree; +} + +export function createNodeIdForExecutable(executable: Executable): TreeNodeId { + return executable.executableId; +} + +export function createExecutableIdFromNodeId(nodeId: TreeNodeId): ExecutableId { + return nodeId; +} + +function parseCategoryRecursively( + parentCategory: Category, +): NodeMetadata[] { + return [ + ...createCategoryNodes(parentCategory.subcategories), + ...createScriptNodes(parentCategory.scripts), + ]; +} + +function createScriptNodes(scripts: ReadonlyArray +./UseExpandCollapseAnimation diff --git a/src/presentation/components/Shared/ExpandCollapse/UseExpandCollapseAnimation.ts b/src/presentation/components/Shared/ExpandCollapse/UseExpandCollapseAnimation.ts new file mode 100644 index 00000000..775ef7ff --- /dev/null +++ b/src/presentation/components/Shared/ExpandCollapse/UseExpandCollapseAnimation.ts @@ -0,0 +1,223 @@ +import { PlatformTimer } from '@/application/Common/Timing/PlatformTimer'; +import type { Timer } from '@/application/Common/Timing/Timer'; + +export type AnimationFunction = (element: Element) => Promise; + +export function useExpandCollapseAnimation( + timer: Timer = PlatformTimer, +): { + readonly collapse: AnimationFunction; + readonly expand: AnimationFunction; + } { + return { + collapse: (element: Element) => animateCollapse(element, timer), + expand: (element: Element) => animateExpand(element, timer), + }; +} + +function animateCollapse( + element: Element, + timer: Timer, +): Promise { + return new Promise((resolve) => { + assertElementIsHTMLElement(element); + applyStyleMutations(element, (elementStyle) => { + const measuredStyles = captureTransitionDimensions(element, elementStyle); + setTransitionPropertiesToHidden(elementStyle); + hideOverflow(elementStyle); + triggerElementRepaint(element); + setTransitionPropertiesToElementDimensions(elementStyle, measuredStyles); + startTransition(elementStyle, timer).then(() => { + elementStyle.restoreOriginalStyles(); + resolve(); + }); + }); + }); +} + +function animateExpand( + element: Element, + timer: Timer, +): Promise { + return new Promise((resolve) => { + assertElementIsHTMLElement(element); + applyStyleMutations(element, (elementStyle) => { + const measuredStyles = captureTransitionDimensions(element, elementStyle); + setTransitionPropertiesToElementDimensions(elementStyle, measuredStyles); + hideOverflow(elementStyle); + triggerElementRepaint(element); + setTransitionPropertiesToHidden(elementStyle); + startTransition(elementStyle, timer).then(() => { + elementStyle.restoreOriginalStyles(); + resolve(); + }); + }); + }); +} + +export const TRANSITION_DURATION_MILLISECONDS = 300; + +const TRANSITION_EASING_FUNCTION = 'ease-in-out'; + +function startTransition( + styleMutator: ElementStyleMutator, + timer: Timer, +): Promise { + return new Promise((resolve) => { + styleMutator.changeStyle('transition', createTransitionStyleValue()); + timer.setTimeout(() => resolve(), TRANSITION_DURATION_MILLISECONDS); + }); +} + +interface ElementStyleMutator { + readonly restoreOriginalStyles: () => void; + readonly changeStyle: (key: MutatedStyleProperty, value: string) => void; +} + +function applyStyleMutations( + element: HTMLElement, + mutator: (elementStyle: ElementStyleMutator) => void, +) { + const originalStyles = getOriginalStyles(element); + const changeStyle = (key: MutatedStyleProperty, value: string) => { + element.style[key] = value; + }; + mutator({ + restoreOriginalStyles: () => restoreOriginalStyles(element, originalStyles), + changeStyle, + }); +} + +function setTransitionPropertiesToHidden(elementStyle: ElementStyleMutator): void { + TransitionedStyleProperties.forEach((key) => { + elementStyle.changeStyle(key, '0px'); + }); +} + +function setTransitionPropertiesToElementDimensions( + elementStyle: ElementStyleMutator, + elementDimensions: TransitionStyleRecords, +): void { + Object.entries(elementDimensions).forEach(([key, value]) => { + elementStyle.changeStyle(key as AnimatedStyleProperty, value); + }); +} + +function hideOverflow(elementStyle: ElementStyleMutator): void { + elementStyle.changeStyle('overflow', 'hidden'); +} + +function createTransitionStyleValue(): string { + const transitions = new Array(); + TransitionedStyleProperties.forEach((key) => { + transitions.push(`${getCssStyleName(key)} ${TRANSITION_DURATION_MILLISECONDS}ms ${TRANSITION_EASING_FUNCTION}`); + }); + return transitions.join(', '); +} + +function captureTransitionDimensions( + element: Readonly, + styleMutator: ElementStyleMutator, +): TransitionStyleRecords { + let styles: TransitionStyleRecords | undefined; + executeActionWithTemporaryVisibility( + element.style, + styleMutator, + () => { + styles = { + height: `${element.offsetHeight}px`, + paddingTop: element.style.paddingTop || getElementComputedStylePropertyValue(element, 'padding-top'), + paddingBottom: element.style.paddingBottom || getElementComputedStylePropertyValue(element, 'padding-bottom'), + }; + }, + ); + return styles as TransitionStyleRecords; +} + +function triggerElementRepaint( + element: Readonly, +): void { + element.offsetHeight; // eslint-disable-line @typescript-eslint/no-unused-expressions +} + +function getElementComputedStylePropertyValue(element: Element, style: string) { + return getComputedStyle(element, null).getPropertyValue(style); +} + +function executeActionWithTemporaryVisibility( + readableStyle: Readonly, + styleMutator: ElementStyleMutator, + actionWhileRendered: () => void, +) { + const { + visibility: initialVisibility, + display: initialDisplay, + } = readableStyle; + styleMutator.changeStyle('visibility', 'hidden'); + styleMutator.changeStyle('display', ''); + try { + actionWhileRendered(); + } finally { + styleMutator.changeStyle('visibility', initialVisibility); + styleMutator.changeStyle('display', initialDisplay); + } +} + +function getOriginalStyles(element: HTMLElement): MutatedStyleProperties { + const records = {} as MutatedStyleProperties; + MutatedStylePropertiesDuringAnimation.forEach((key) => { + records[key] = element.style[key]; + }); + return records; +} + +function restoreOriginalStyles( + element: HTMLElement, + originalStyles: MutatedStyleProperties, +): void { + Object.entries(originalStyles).forEach(([key, value]) => { + element.style[key as MutatedStyleProperty] = value; + }); +} + +function getCssStyleName(style: AnimatedStyleProperty): string { + const cssPropertyNames: TransitionStyleRecords = { + height: 'height', + paddingTop: 'padding-top', + paddingBottom: 'padding-bottom', + }; + return cssPropertyNames[style]; +} + +function assertElementIsHTMLElement( + element: Element, +): asserts element is HTMLElement { + if (!element) { + throw new Error('Element was not found'); + } + if (!(element instanceof HTMLElement)) { + throw new Error('Element is not an HTMLElement'); + } +} + +const TransitionedStyleProperties = [ + 'height', + 'paddingTop', + 'paddingBottom', +] as const; + +const MutatedStylePropertiesDuringAnimation = [ + ...TransitionedStyleProperties, + 'transition', + 'overflow', + 'visibility', + 'display', +] as const; + +type MutatedStyleProperty = typeof MutatedStylePropertiesDuringAnimation[number]; + +export type MutatedStyleProperties = Record; + +type AnimatedStyleProperty = typeof TransitionedStyleProperties[number]; + +type TransitionStyleRecords = Record; diff --git a/src/presentation/components/Shared/FlatButton.vue b/src/presentation/components/Shared/FlatButton.vue new file mode 100644 index 00000000..9ca98fd3 --- /dev/null +++ b/src/presentation/components/Shared/FlatButton.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/src/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard.ts b/src/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard.ts new file mode 100644 index 00000000..2a203481 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard.ts @@ -0,0 +1,15 @@ +import type { Clipboard } from './Clipboard'; + +export type NavigatorClipboard = typeof globalThis.navigator.clipboard; + +export class BrowserClipboard implements Clipboard { + constructor( + private readonly navigatorClipboard: NavigatorClipboard = globalThis.navigator.clipboard, + ) { + + } + + public async copyText(text: string): Promise { + await this.navigatorClipboard.writeText(text); + } +} diff --git a/src/presentation/components/Shared/Hooks/Clipboard/Clipboard.ts b/src/presentation/components/Shared/Hooks/Clipboard/Clipboard.ts new file mode 100644 index 00000000..a4dc64a7 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Clipboard/Clipboard.ts @@ -0,0 +1,3 @@ +export interface Clipboard { + copyText(text: string): Promise; +} diff --git a/src/presentation/components/Shared/Hooks/Clipboard/UseClipboard.ts b/src/presentation/components/Shared/Hooks/Clipboard/UseClipboard.ts new file mode 100644 index 00000000..e43f59d3 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Clipboard/UseClipboard.ts @@ -0,0 +1,13 @@ +import type { FunctionKeys } from '@/TypeHelpers'; +import { BrowserClipboard } from './BrowserClipboard'; +import type { Clipboard } from './Clipboard'; + +export function useClipboard(clipboard: Clipboard = new BrowserClipboard()) { + // Bind functions for direct use from destructured assignments such as `const { .. } = ...`. + const functionKeys: readonly FunctionKeys[] = ['copyText']; + functionKeys.forEach((functionName) => { + const fn = clipboard[functionName]; + clipboard[functionName] = fn.bind(clipboard); + }); + return clipboard; +} diff --git a/src/presentation/components/Shared/Hooks/Common/LifecycleHook.ts b/src/presentation/components/Shared/Hooks/Common/LifecycleHook.ts new file mode 100644 index 00000000..fb250344 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Common/LifecycleHook.ts @@ -0,0 +1,8 @@ +/* + These types are used to abstract Vue Lifecycle injection APIs + (e.g., onBeforeMount, onUnmount) for better testability. +*/ + +export type LifecycleHook = (callback: LifecycleHookCallback) => void; + +export type LifecycleHookCallback = () => void; diff --git a/src/presentation/components/Shared/Hooks/Dialog/ClientDialogFactory.ts b/src/presentation/components/Shared/Hooks/Dialog/ClientDialogFactory.ts new file mode 100644 index 00000000..821821d6 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Dialog/ClientDialogFactory.ts @@ -0,0 +1,49 @@ +import type { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment'; +import type { Dialog } from '@/presentation/common/Dialog'; +import { BrowserDialog } from '@/infrastructure/Dialog/Browser/BrowserDialog'; +import { decorateWithLogging } from '@/infrastructure/Dialog/LoggingDialogDecorator'; +import { ClientLoggerFactory } from '../Log/ClientLoggerFactory'; + +export function createEnvironmentSpecificLoggedDialog( + environment: RuntimeEnvironment, + dialogLoggingDecorator: DialogLoggingDecorator = ClientLoggingDecorator, + windowInjectedDialogFactory: WindowDialogCreationFunction = () => globalThis.window.dialog, + browserDialogFactory: BrowserDialogCreationFunction = () => new BrowserDialog(), +): Dialog { + const dialog = determineDialogBasedOnEnvironment( + environment, + windowInjectedDialogFactory, + browserDialogFactory, + ); + const loggingDialog = dialogLoggingDecorator(dialog); + return loggingDialog; +} + +function determineDialogBasedOnEnvironment( + environment: RuntimeEnvironment, + windowInjectedDialogFactory: WindowDialogCreationFunction = () => globalThis.window.dialog, + browserDialogFactory: BrowserDialogCreationFunction = () => new BrowserDialog(), +): Dialog { + if (!environment.isRunningAsDesktopApplication) { + return browserDialogFactory(); + } + const windowDialog = windowInjectedDialogFactory(); + if (!windowDialog) { + throw new Error([ + 'Failed to retrieve Dialog API from window object in desktop environment.', + 'This may indicate that the Dialog API is either not implemented or not correctly exposed in the current desktop environment.', + ].join('\n')); + } + return windowDialog; +} + +export type WindowDialogCreationFunction = () => Dialog | undefined; + +export type BrowserDialogCreationFunction = () => Dialog; + +export type DialogLoggingDecorator = (dialog: Dialog) => Dialog; + +const ClientLoggingDecorator: DialogLoggingDecorator = (dialog) => decorateWithLogging( + dialog, + ClientLoggerFactory.Current.logger, +); diff --git a/src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts b/src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts new file mode 100644 index 00000000..8013d657 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts @@ -0,0 +1,14 @@ +import type { Dialog } from '@/presentation/common/Dialog'; +import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory'; +import { createEnvironmentSpecificLoggedDialog } from './ClientDialogFactory'; + +export function useDialog( + factory: DialogFactory = () => createEnvironmentSpecificLoggedDialog(CurrentEnvironment), +) { + const dialog = factory(); + return { + dialog, + }; +} + +export type DialogFactory = () => Dialog; diff --git a/src/presentation/components/Shared/Hooks/Log/ClientLoggerFactory.ts b/src/presentation/components/Shared/Hooks/Log/ClientLoggerFactory.ts new file mode 100644 index 00000000..e05ed00c --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Log/ClientLoggerFactory.ts @@ -0,0 +1,53 @@ +import type { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment'; +import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger'; +import type { Logger } from '@/application/Common/Log/Logger'; +import { NoopLogger } from '@/infrastructure/Log/NoopLogger'; +import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger'; +import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory'; +import type { LoggerFactory } from './LoggerFactory'; + +export class ClientLoggerFactory implements LoggerFactory { + public static readonly Current: LoggerFactory = new ClientLoggerFactory(); + + public readonly logger: Logger; + + protected constructor( + environment: RuntimeEnvironment = CurrentEnvironment, + windowAccessor: WindowAccessor = () => globalThis.window, + noopLoggerFactory: LoggerCreationFunction = () => new NoopLogger(), + windowInjectedLoggerFactory: LoggerCreationFunction = () => new WindowInjectedLogger(), + consoleLoggerFactory: LoggerCreationFunction = () => new ConsoleLogger(), + ) { + if (isUnitOrIntegrationTests(environment, windowAccessor)) { + this.logger = noopLoggerFactory(); // keep the test outputs clean + return; + } + if (environment.isRunningAsDesktopApplication) { + this.logger = windowInjectedLoggerFactory(); + return; + } + if (environment.isNonProduction) { + this.logger = consoleLoggerFactory(); + return; + } + this.logger = noopLoggerFactory(); + } +} + +export type WindowAccessor = () => OptionalWindow; + +export type LoggerCreationFunction = () => Logger; + +type OptionalWindow = Window | undefined | null; + +function isUnitOrIntegrationTests( + environment: RuntimeEnvironment, + windowAccessor: WindowAccessor, +): boolean { + /* + In a desktop application context, Electron preloader process inject a logger into + the global window object. If we're in a desktop (Node) environment and the logger isn't + injected, it indicates a testing environment. + */ + return environment.isRunningAsDesktopApplication && !windowAccessor()?.log; +} diff --git a/src/presentation/components/Shared/Hooks/Log/LoggerFactory.ts b/src/presentation/components/Shared/Hooks/Log/LoggerFactory.ts new file mode 100644 index 00000000..3e9bec80 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Log/LoggerFactory.ts @@ -0,0 +1,5 @@ +import type { Logger } from '@/application/Common/Log/Logger'; + +export interface LoggerFactory { + readonly logger: Logger; +} diff --git a/src/presentation/components/Shared/Hooks/Log/UseLogger.ts b/src/presentation/components/Shared/Hooks/Log/UseLogger.ts new file mode 100644 index 00000000..bfb33793 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Log/UseLogger.ts @@ -0,0 +1,8 @@ +import { ClientLoggerFactory } from './ClientLoggerFactory'; +import type { LoggerFactory } from './LoggerFactory'; + +export function useLogger(factory: LoggerFactory = ClientLoggerFactory.Current) { + return { + log: factory.logger, + }; +} diff --git a/src/presentation/components/Shared/Hooks/README.md b/src/presentation/components/Shared/Hooks/README.md new file mode 100644 index 00000000..c4c470c1 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/README.md @@ -0,0 +1,5 @@ +# Hooks + +This folder contains shared hooks used throughout the application. + +To use the hooks, prefer using Vue-native `provide` / `inject` pattern to keep the components independently testable without side-effect. diff --git a/src/presentation/components/Shared/Hooks/Resize/UseAnimationFrameLimiter.ts b/src/presentation/components/Shared/Hooks/Resize/UseAnimationFrameLimiter.ts new file mode 100644 index 00000000..92f1aff4 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Resize/UseAnimationFrameLimiter.ts @@ -0,0 +1,41 @@ +import { onBeforeUnmount } from 'vue'; + +export function useAnimationFrameLimiter( + cancelAnimationFrame: CancelAnimationFrameFunction = window.cancelAnimationFrame, + requestAnimationFrame: RequestAnimationFrameFunction = window.requestAnimationFrame, + onTeardown: RegisterTeardownCallbackFunction = onBeforeUnmount, +): AnimationFrameLimiter { + let requestId: AnimationFrameId | null = null; + const cancelNextFrame = () => { + if (requestId === null) { + return; + } + cancelAnimationFrame(requestId); + }; + const resetNextFrame = (callback: AnimationFrameRequestCallback) => { + cancelNextFrame(); + requestId = requestAnimationFrame(callback); + }; + onTeardown(() => { + cancelNextFrame(); + }); + return { + cancelNextFrame, + resetNextFrame, + }; +} + +export type CancelAnimationFrameFunction = typeof window.cancelAnimationFrame; + +export type RequestAnimationFrameFunction = (callback: AnimationFrameRequestCallback) => number; + +export type RegisterTeardownCallbackFunction = (callback: () => void) => void; + +export type AnimationFrameId = ReturnType; + +export type AnimationFrameRequestCallback = () => void; + +export interface AnimationFrameLimiter { + cancelNextFrame(): void; + resetNextFrame(callback: AnimationFrameRequestCallback): void; +} diff --git a/src/presentation/components/Shared/Hooks/Resize/UseResizeObserver.ts b/src/presentation/components/Shared/Hooks/Resize/UseResizeObserver.ts new file mode 100644 index 00000000..be7e5ac1 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Resize/UseResizeObserver.ts @@ -0,0 +1,66 @@ +import { + onBeforeMount, onBeforeUnmount, + watch, type Ref, +} from 'vue'; +import { throttle, type ThrottleFunction } from '@/application/Common/Timing/Throttle'; +import { useResizeObserverPolyfill } from './UseResizeObserverPolyfill'; +import { useAnimationFrameLimiter } from './UseAnimationFrameLimiter'; +import type { LifecycleHook } from '../Common/LifecycleHook'; + +export function useResizeObserver( + config: ResizeObserverConfig, + usePolyfill = useResizeObserverPolyfill, + useFrameLimiter = useAnimationFrameLimiter, + throttler: ThrottleFunction = throttle, + onSetup: LifecycleHook = onBeforeMount, + onTeardown: LifecycleHook = onBeforeUnmount, +) { + const { resetNextFrame, cancelNextFrame } = useFrameLimiter(); + // This prevents the 'ResizeObserver loop completed with undelivered notifications' error when + // the browser can't process all observations within one animation frame. + // Reference: https://github.com/WICG/resize-observer/issues/38 + + const { resizeObserverReady } = usePolyfill(); + // This ensures compatibility with ancient browsers. All modern browsers support ResizeObserver. + // Compatibility info: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#browser_compatibility + + const throttledCallback = throttler(config.observeCallback, config.throttleInMs); + // Throttling enhances performance during rapid changes such as window resizing. + + let observer: ResizeObserver | null; + + const disposeObserver = () => { + cancelNextFrame(); + observer?.disconnect(); + observer = null; + }; + + onSetup(() => { + watch(() => config.observedElementRef.value, (element) => { + if (!element) { + disposeObserver(); + return; + } + resizeObserverReady.then((createObserver) => { + disposeObserver(); + observer = createObserver((...args) => { + resetNextFrame(() => throttledCallback(...args)); + }); + observer.observe(element, config?.observeOptions); + }); + }, { immediate: true }); + }); + + onTeardown(() => { + disposeObserver(); + }); +} + +export interface ResizeObserverConfig { + readonly observedElementRef: ObservedElementReference; + readonly throttleInMs: number; + readonly observeCallback: ResizeObserverCallback; + readonly observeOptions?: ResizeObserverOptions; +} + +export type ObservedElementReference = Readonly>; diff --git a/src/presentation/components/Shared/Hooks/Resize/UseResizeObserverPolyfill.ts b/src/presentation/components/Shared/Hooks/Resize/UseResizeObserverPolyfill.ts new file mode 100644 index 00000000..ab3819b5 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/Resize/UseResizeObserverPolyfill.ts @@ -0,0 +1,33 @@ +import { onMounted } from 'vue'; +import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; + +// AsyncLazy ensures single load of the ResizeObserver polyfill, +// even when multiple calls are made simultaneously. +const polyfillLoader = new AsyncLazy(async () => { + if ('ResizeObserver' in window) { + return window.ResizeObserver; + } + const module = await import('@juggle/resize-observer'); + globalThis.window.ResizeObserver = module.ResizeObserver; + return module.ResizeObserver; +}); + +async function polyfillResizeObserver(): Promise { + return polyfillLoader.getValue(); +} + +interface ResizeObserverCreator { + ( + ...args: ConstructorParameters + ): ResizeObserver; +} + +export function useResizeObserverPolyfill() { + const resizeObserverReady = new Promise((resolve) => { + onMounted(async () => { + await polyfillResizeObserver(); + resolve((args) => new ResizeObserver(args)); + }); + }); + return { resizeObserverReady }; +} diff --git a/src/presentation/components/Shared/Hooks/UseApplication.ts b/src/presentation/components/Shared/Hooks/UseApplication.ts new file mode 100644 index 00000000..22a85421 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseApplication.ts @@ -0,0 +1,8 @@ +import type { IApplication } from '@/domain/IApplication'; + +export function useApplication(application: IApplication) { + return { + application, + projectDetails: application.projectDetails, + }; +} diff --git a/src/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.ts b/src/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.ts new file mode 100644 index 00000000..4ce57125 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.ts @@ -0,0 +1,86 @@ +import { + onBeforeUnmount, + shallowRef, + watch, + type Ref, +} from 'vue'; +import type { LifecycleHook } from './Common/LifecycleHook'; + +export interface UseEventListener { + ( + onTeardown?: LifecycleHook, + ): TargetEventListener; +} + +export const useAutoUnsubscribedEventListener: UseEventListener = ( + onTeardown = onBeforeUnmount, +) => ({ + startListening: (eventTargetSource, eventType, eventHandler) => { + const eventTargetRef = isEventTarget(eventTargetSource) + ? shallowRef(eventTargetSource) + : eventTargetSource; + return startListeningRef( + eventTargetRef, + eventType, + eventHandler, + onTeardown, + ); + }, +}); + +type EventTargetRef = Readonly>; + +type EventTargetOrRef = EventTargetRef | EventTarget; + +function isEventTarget(obj: EventTargetOrRef): obj is EventTarget { + return obj instanceof EventTarget; +} + +export interface TargetEventListener { + startListening( + eventTargetSource: EventTargetOrRef, + eventType: TEvent, + eventHandler: (event: HTMLElementEventMap[TEvent]) => void, + ): void; +} + +function startListeningRef( + eventTargetRef: Readonly>, + eventType: TEvent, + eventHandler: (event: HTMLElementEventMap[TEvent]) => void, + onTeardown: LifecycleHook, +): void { + const eventListenerManager = new EventListenerManager(); + watch(() => eventTargetRef.value, (element) => { + eventListenerManager.removeListenerIfExists(); + if (!element) { + return; + } + eventListenerManager.addListener(element, eventType, eventHandler); + }, { immediate: true }); + + onTeardown(() => { + eventListenerManager.removeListenerIfExists(); + }); +} + +class EventListenerManager { + private removeListener: (() => void) | null = null; + + public removeListenerIfExists() { + if (this.removeListener === null) { + return; + } + this.removeListener(); + this.removeListener = null; + } + + public addListener( + eventTarget: EventTarget, + eventType: TEvent, + eventHandler: (event: HTMLElementEventMap[TEvent]) => void, + ) { + eventTarget.addEventListener(eventType, eventHandler); + this.removeListener = () => eventTarget.removeEventListener(eventType, eventHandler); + } +} diff --git a/src/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents.ts b/src/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents.ts new file mode 100644 index 00000000..c2bb6ea0 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents.ts @@ -0,0 +1,21 @@ +import { onUnmounted } from 'vue'; +import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection'; +import type { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection'; +import type { LifecycleHook } from './Common/LifecycleHook'; + +export function useAutoUnsubscribedEvents( + events: IEventSubscriptionCollection = new EventSubscriptionCollection(), + onTeardown: LifecycleHook = onUnmounted, +) { + if (events.subscriptionCount > 0) { + throw new Error('there are existing subscriptions, this may lead to side-effects'); + } + + onTeardown(() => { + events.unsubscribeAll(); + }); + + return { + events, + }; +} diff --git a/src/presentation/components/Shared/Hooks/UseCodeRunner.ts b/src/presentation/components/Shared/Hooks/UseCodeRunner.ts new file mode 100644 index 00000000..50b85e75 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseCodeRunner.ts @@ -0,0 +1,9 @@ +import type { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; + +export function useCodeRunner( + window: WindowVariables = globalThis.window, +) { + return { + codeRunner: window.codeRunner, + }; +} diff --git a/src/presentation/components/Shared/Hooks/UseCollectionState.ts b/src/presentation/components/Shared/Hooks/UseCollectionState.ts new file mode 100644 index 00000000..61aeae23 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseCollectionState.ts @@ -0,0 +1,70 @@ +import { shallowRef, shallowReadonly } from 'vue'; +import type { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext'; +import type { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import type { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection'; + +export function useCollectionState( + context: IApplicationContext, + events: IEventSubscriptionCollection, +) { + const currentState = shallowRef(context.state); + events.register([ + context.contextChanged.on((event) => { + currentState.value = event.newState; + }), + ]); + + const defaultSettings: IStateCallbackSettings = { + immediate: false, + }; + function onStateChange( + handler: NewStateEventHandler, + settings: Partial = defaultSettings, + ) { + events.register([ + context.contextChanged.on((event) => { + handler(event.newState, event.oldState); + }), + ]); + const defaultedSettings: IStateCallbackSettings = { + ...defaultSettings, + ...settings, + }; + if (defaultedSettings.immediate) { + handler(context.state, undefined); + } + } + + function modifyCurrentState(mutator: StateModifier) { + mutator(context.state); + } + + function modifyCurrentContext(mutator: ContextModifier) { + mutator(context); + } + + return { + modifyCurrentState, + modifyCurrentContext, + onStateChange, + currentContext: context as IReadOnlyApplicationContext, + currentState: shallowReadonly(currentState), + }; +} + +export type NewStateEventHandler = ( + newState: IReadOnlyCategoryCollectionState, + oldState: IReadOnlyCategoryCollectionState | undefined, +) => void; + +export interface IStateCallbackSettings { + readonly immediate: boolean; +} + +export type StateModifier = ( + state: ICategoryCollectionState, +) => void; + +export type ContextModifier = ( + state: IApplicationContext, +) => void; diff --git a/src/presentation/components/Shared/Hooks/UseCurrentCode.ts b/src/presentation/components/Shared/Hooks/UseCurrentCode.ts new file mode 100644 index 00000000..34c04956 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseCurrentCode.ts @@ -0,0 +1,32 @@ +import { ref } from 'vue'; +import type { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode'; +import type { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection'; +import { useCollectionState } from './UseCollectionState'; + +export function useCurrentCode( + state: ReturnType, + events: IEventSubscriptionCollection, +) { + const { onStateChange } = state; + + const currentCode = ref(''); + + onStateChange((newState) => { + updateCurrentCode(newState.code.current); + subscribeToCodeChanges(newState.code); + }, { immediate: true }); + + function subscribeToCodeChanges(code: IApplicationCode) { + events.unsubscribeAllAndRegister([ + code.changed.on((newCode) => updateCurrentCode(newCode.code)), + ]); + } + + function updateCurrentCode(newCode: string) { + currentCode.value = newCode; + } + + return { + currentCode, + }; +} diff --git a/src/presentation/components/Shared/Hooks/UseRuntimeEnvironment.ts b/src/presentation/components/Shared/Hooks/UseRuntimeEnvironment.ts new file mode 100644 index 00000000..8aa0a84a --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseRuntimeEnvironment.ts @@ -0,0 +1,5 @@ +import type { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment'; + +export function useRuntimeEnvironment(environment: RuntimeEnvironment) { + return environment; +} diff --git a/src/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector.ts b/src/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector.ts new file mode 100644 index 00000000..32264d76 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector.ts @@ -0,0 +1,9 @@ +import type { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; + +export function useScriptDiagnosticsCollector( + window: Partial = globalThis.window, +) { + return { + scriptDiagnosticsCollector: window?.scriptDiagnosticsCollector, + }; +} diff --git a/src/presentation/components/Shared/Hooks/UseUserSelectionState.ts b/src/presentation/components/Shared/Hooks/UseUserSelectionState.ts new file mode 100644 index 00000000..499467b3 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseUserSelectionState.ts @@ -0,0 +1,48 @@ +import { shallowReadonly, shallowRef, triggerRef } from 'vue'; +import type { ReadonlyUserSelection, UserSelection } from '@/application/Context/State/Selection/UserSelection'; +import type { useAutoUnsubscribedEvents } from './UseAutoUnsubscribedEvents'; +import type { useCollectionState } from './UseCollectionState'; + +export function useUserSelectionState( + collectionState: ReturnType, + autoUnsubscribedEvents: ReturnType, +) { + const { events } = autoUnsubscribedEvents; + const { onStateChange, modifyCurrentState, currentState } = collectionState; + + const currentSelection = shallowRef(currentState.value.selection); + + onStateChange((state) => { + updateSelection(state.selection); + events.unsubscribeAllAndRegister([ + state.selection.scripts.changed.on(() => { + updateSelection(state.selection); + }), + ]); + }, { immediate: true }); + + function modifyCurrentSelection(mutator: SelectionModifier) { + modifyCurrentState((state) => { + mutator(state.selection); + }); + } + + function updateSelection(newSelection: ReadonlyUserSelection) { + if (currentSelection.value === newSelection) { + // Do not trust Vue tracking, the changed selection object + // reference may stay same for same collection. + triggerRef(currentSelection); + } else { + currentSelection.value = newSelection; + } + } + + return { + currentSelection: shallowReadonly(currentSelection), + modifyCurrentSelection, + }; +} + +export type SelectionModifier = ( + state: UserSelection, +) => void; diff --git a/src/presentation/components/Shared/Icon/AppIcon.vue b/src/presentation/components/Shared/Icon/AppIcon.vue new file mode 100644 index 00000000..cd026c35 --- /dev/null +++ b/src/presentation/components/Shared/Icon/AppIcon.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/presentation/components/Shared/Icon/IconName.ts b/src/presentation/components/Shared/Icon/IconName.ts new file mode 100644 index 00000000..8c97c488 --- /dev/null +++ b/src/presentation/components/Shared/Icon/IconName.ts @@ -0,0 +1,27 @@ +export const IconNames = [ + 'magnifying-glass', + 'copy', + 'circle-info', + 'user-secret', + 'tag', + 'github', + 'face-smile', + 'globe', + 'desktop', + 'xmark', + 'battery-half', + 'battery-full', + 'folder', + 'folder-open', + 'left-right', + 'file-arrow-down', + 'floppy-disk', + 'play', + 'lightbulb', + 'square-check', + 'triangle-exclamation', + 'rotate-left', + 'shield', +] as const; + +export type IconName = typeof IconNames[number]; diff --git a/src/presentation/components/Shared/Icon/UseSvgLoader.ts b/src/presentation/components/Shared/Icon/UseSvgLoader.ts new file mode 100644 index 00000000..b20bed50 --- /dev/null +++ b/src/presentation/components/Shared/Icon/UseSvgLoader.ts @@ -0,0 +1,93 @@ +import { + type WatchSource, shallowReadonly, ref, watch, +} from 'vue'; +import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; +import type { IconName } from './IconName'; + +export function useSvgLoader( + iconWatcher: WatchSource, + loaders: FileLoaders = RawSvgLoaders, +) { + const svgContent = ref(''); + + watch(iconWatcher, async (iconName) => { + svgContent.value = await lazyLoadSvg(iconName, loaders); + }, { immediate: true }); + + return { + svgContent: shallowReadonly(svgContent), + }; +} + +export function clearIconCache() { + LazyIconCache.clear(); +} + +export type FileLoaders = Record Promise>; + +const LazyIconCache = new Map>(); + +async function lazyLoadSvg(name: IconName, loaders: FileLoaders): Promise { + let iconLoader = LazyIconCache.get(name); + if (!iconLoader) { + iconLoader = new AsyncLazy(() => loadSvg(name, loaders)); + LazyIconCache.set(name, iconLoader); + } + const icon = await iconLoader.getValue(); + return icon; +} + +async function loadSvg(name: IconName, loaders: FileLoaders): Promise { + const iconPath = `/assets/icons/${name}.svg`; + const loader = loaders[iconPath]; + if (!loader) { + throw new Error(`missing icon for "${name}" in "${iconPath}"`); + } + const svgContent = await loader(); + const modifiedContent = modifySvg(svgContent); + return modifiedContent; +} + +const RawSvgLoaders: FileLoaders = import.meta.glob('@/presentation/assets/icons/**/*.svg', { + query: '?raw', + import: 'default', + /* + Using `eager: true` to preload all icons. + Pros: + - Speed: Icons are instantly accessible post-initial load. + Cons: + - Increased initial load time due to preloading of all icons. + - Increased bundle size. + */ + eager: false, +}); + +function modifySvg(svgSource: string): string { + const parser = new globalThis.window.DOMParser(); + const doc = parser.parseFromString(svgSource, 'image/svg+xml'); + let svgRoot = doc.documentElement; + svgRoot = removeSvgComments(svgRoot); + svgRoot = fillSvgCurrentColor(svgRoot); + return new XMLSerializer() + .serializeToString(svgRoot); +} + +function removeSvgComments(svgRoot: HTMLElement): HTMLElement { + const comments = Array.from(svgRoot.childNodes).filter( + (node) => node.nodeType === Node.COMMENT_NODE, + ); + for (const comment of comments) { + svgRoot.removeChild(comment); + } + Array.from(svgRoot.children).forEach((child) => { + removeSvgComments(child as HTMLElement); + }); + return svgRoot; +} + +function fillSvgCurrentColor(svgRoot: HTMLElement): HTMLElement { + svgRoot.querySelectorAll('path').forEach((el: Element) => { + el.setAttribute('fill', 'currentColor'); + }); + return svgRoot; +} diff --git a/src/presentation/components/Shared/Modal/Hooks/ScrollLock/ScrollDomStateAccessor.ts b/src/presentation/components/Shared/Modal/Hooks/ScrollLock/ScrollDomStateAccessor.ts new file mode 100644 index 00000000..c4c820bd --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/ScrollLock/ScrollDomStateAccessor.ts @@ -0,0 +1,21 @@ +export interface ScrollDomStateAccessor { + bodyStyleOverflowX: string; + bodyStyleOverflowY: string; + htmlScrollLeft: number; + htmlScrollTop: number; + bodyStyleLeft: string; + bodyStyleTop: string; + bodyStylePosition: string; + bodyStyleWidth: string; + bodyStyleHeight: string; + readonly bodyComputedMarginLeft: string; + readonly bodyComputedMarginRight: string; + readonly bodyComputedMarginTop: string; + readonly bodyComputedMarginBottom: string; + readonly htmlScrollWidth: number; + readonly htmlScrollHeight: number; + readonly htmlClientWidth: number; + readonly htmlClientHeight: number; + readonly htmlOffsetWidth: number; + readonly htmlOffsetHeight: number; +} diff --git a/src/presentation/components/Shared/Modal/Hooks/ScrollLock/UseLockBodyBackgroundScroll.ts b/src/presentation/components/Shared/Modal/Hooks/ScrollLock/UseLockBodyBackgroundScroll.ts new file mode 100644 index 00000000..e6de08c3 --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/ScrollLock/UseLockBodyBackgroundScroll.ts @@ -0,0 +1,318 @@ +import { type Ref, onBeforeUnmount, watch } from 'vue'; +import { getWindowDomState } from './WindowScrollDomStateAccessor'; +import type { ScrollDomStateAccessor } from './ScrollDomStateAccessor'; + +export function useLockBodyBackgroundScroll( + isActive: Ref, + dom: ScrollDomStateAccessor = getWindowDomState(), +) { + let isBlocked = false; + + const applyScrollLock = () => { + ScrollLockMutators.forEach((mutator) => { + mutator.onBeforeBlock(dom); + }); + ScrollLockMutators.forEach((mutator) => { + mutator.onBlock(dom); + }); + isBlocked = true; + }; + + function revertScrollLock() { + if (!isBlocked) { + return; + } + ScrollLockMutators.forEach((mutator) => { + mutator.onUnblock(dom); + }); + } + + watch(isActive, (shouldBlock) => { + if (shouldBlock) { + applyScrollLock(); + } else { + revertScrollLock(); + } + }, { immediate: true }); + + onBeforeUnmount(() => { + revertScrollLock(); + }); +} + +interface ScrollStateManipulator { + onBeforeBlock(dom: ScrollDomStateAccessor): void; + onBlock(dom: ScrollDomStateAccessor): void; + onUnblock(dom: ScrollDomStateAccessor): void; +} + +function createScrollStateManipulator( + propertyMutator: DomPropertyMutator, +): ScrollStateManipulator { + let state: TStoredState | undefined; + let restoreAction: ScrollRevertAction | undefined; + return { + onBeforeBlock: (dom) => { + state = propertyMutator.storeInitialState(dom); + }, + onBlock: (dom) => { + verifyStateInitialization(state); + restoreAction = propertyMutator.onBlock(state, dom); + }, + onUnblock: (dom) => { + switch (restoreAction) { + case ScrollRevertAction.RestoreRequired: + verifyStateInitialization(state); + propertyMutator.restoreStateOnUnblock(state, dom); + break; + case ScrollRevertAction.SkipRestore: + return; + case undefined: + throw new Error('Undefined restore action'); + default: + throw new Error(`Unknown action: ${ScrollRevertAction[restoreAction]}`); + } + }, + }; +} + +function verifyStateInitialization( + value: TState | undefined, +): asserts value is TState { + if (value === null || value === undefined) { + throw new Error('Previous state not found. Ensure state initialization before mutation operations.'); + } +} + +const HtmlScrollLeft: DomPropertyMutator<{ + readonly htmlScrollLeft: number; +}> = { + storeInitialState: (dom) => ({ + htmlScrollLeft: dom.htmlScrollLeft, + }), + onBlock: () => ScrollRevertAction.RestoreRequired, + restoreStateOnUnblock: (initialState, dom) => { + dom.htmlScrollLeft = initialState.htmlScrollLeft; + }, +}; + +const BodyStyleLeft: DomPropertyMutator<{ + readonly htmlScrollLeft: number; + readonly bodyStyleLeft: string; +}> = { + storeInitialState: (dom) => ({ + htmlScrollLeft: dom.htmlScrollLeft, + bodyStyleLeft: dom.bodyStyleLeft, + }), + onBlock: (initialState, dom) => { + if (initialState.htmlScrollLeft === 0) { + return ScrollRevertAction.SkipRestore; + } + dom.bodyStyleLeft = `-${initialState.htmlScrollLeft}px`; + return ScrollRevertAction.RestoreRequired; + }, + restoreStateOnUnblock: (initialState, dom) => { + dom.bodyStyleLeft = initialState.bodyStyleLeft; + }, +}; + +const BodyStyleTop: DomPropertyMutator<{ + readonly htmlScrollTop: number; + readonly bodyStyleTop: string; +}> = { + storeInitialState: (dom) => ({ + bodyStyleTop: dom.bodyStyleTop, + htmlScrollTop: dom.htmlScrollTop, + }), + onBlock: (initialState, dom) => { + if (initialState.htmlScrollTop === 0) { + return ScrollRevertAction.SkipRestore; + } + dom.bodyStyleTop = `-${initialState.htmlScrollTop}px`; + return ScrollRevertAction.RestoreRequired; + }, + restoreStateOnUnblock: (initialState, dom) => { + dom.bodyStyleTop = initialState.bodyStyleTop; + }, +}; + +const BodyStyleOverflowX: DomPropertyMutator<{ + readonly isHorizontalScrollbarVisible: boolean; + readonly bodyStyleOverflowX: string; +}> = { + storeInitialState: (dom) => ({ + isHorizontalScrollbarVisible: dom.htmlScrollWidth > dom.htmlClientWidth, + bodyStyleOverflowX: dom.bodyStyleOverflowX, + }), + onBlock: (initialState, dom) => { + if (!initialState.isHorizontalScrollbarVisible) { + return ScrollRevertAction.SkipRestore; + } + dom.bodyStyleOverflowX = 'scroll'; + return ScrollRevertAction.RestoreRequired; + }, + restoreStateOnUnblock: (initialState, dom) => { + dom.bodyStyleOverflowX = initialState.bodyStyleOverflowX; + }, +}; + +const BodyStyleOverflowY: DomPropertyMutator<{ + readonly isVerticalScrollbarVisible: boolean; + readonly bodyStyleOverflowY: string; +}> = { + storeInitialState: (dom) => ({ + isVerticalScrollbarVisible: dom.htmlScrollHeight > dom.htmlClientHeight, + bodyStyleOverflowY: dom.bodyStyleOverflowY, + }), + onBlock: (initialState, dom) => { + if (!initialState.isVerticalScrollbarVisible) { + return ScrollRevertAction.SkipRestore; + } + dom.bodyStyleOverflowY = 'scroll'; + return ScrollRevertAction.RestoreRequired; + }, + restoreStateOnUnblock: (initialState, dom) => { + dom.bodyStyleOverflowY = initialState.bodyStyleOverflowY; + }, +}; + +const HtmlScrollTop: DomPropertyMutator<{ + readonly htmlScrollTop: number; +}> = { + storeInitialState: (dom) => ({ + htmlScrollTop: dom.htmlScrollTop, + }), + onBlock: () => ScrollRevertAction.RestoreRequired, + restoreStateOnUnblock: (initialState, dom) => { + dom.htmlScrollTop = initialState.htmlScrollTop; + }, +}; + +const BodyPositionFixed: DomPropertyMutator<{ + readonly bodyStylePosition: string; +}> = { + storeInitialState: (dom) => ({ + bodyStylePosition: dom.bodyStylePosition, + }), + onBlock: (_, dom) => { + dom.bodyStylePosition = 'fixed'; + return ScrollRevertAction.RestoreRequired; + }, + restoreStateOnUnblock: (initialState, dom) => { + dom.bodyStylePosition = initialState.bodyStylePosition; + }, +}; + +const BodyWidth100Percent: DomPropertyMutator<{ + readonly bodyStyleWidth: string; + readonly htmlOffsetWidth: number; + readonly htmlClientWidth: number; +}> = { + storeInitialState: (dom) => ({ + bodyStyleWidth: dom.bodyStyleWidth, + htmlOffsetWidth: dom.htmlOffsetWidth, + htmlClientWidth: dom.htmlClientWidth, + }), + onBlock: (initialState, dom) => { + dom.bodyStyleWidth = calculateAdjustedStyle( + [ + dom.bodyComputedMarginLeft, + dom.bodyComputedMarginRight, + calculateScrollbarGutterStyle(initialState.htmlClientWidth, initialState.htmlOffsetWidth), + ], + ); + return ScrollRevertAction.RestoreRequired; + }, + restoreStateOnUnblock: (initialState, dom) => { + dom.bodyStyleWidth = initialState.bodyStyleWidth; + }, +}; + +const BodyHeight100Percent: DomPropertyMutator<{ + readonly bodyStyleHeight: string; + readonly htmlOffsetHeight: number; + readonly htmlClientHeight: number; +}> = { + storeInitialState: (dom) => ({ + bodyStyleHeight: dom.bodyStyleHeight, + htmlOffsetHeight: dom.htmlOffsetHeight, + htmlClientHeight: dom.htmlClientHeight, + }), + onBlock: (initialState, dom) => { + dom.bodyStyleHeight = calculateAdjustedStyle( + [ + dom.bodyComputedMarginTop, + dom.bodyComputedMarginBottom, + calculateScrollbarGutterStyle(initialState.htmlClientHeight, initialState.htmlOffsetHeight), + ], + ); + return ScrollRevertAction.RestoreRequired; + }, + restoreStateOnUnblock: (initialState, dom) => { + dom.bodyStyleHeight = initialState.bodyStyleHeight; + }, +}; + +const ScrollLockMutators: readonly ScrollStateManipulator[] = [ + createScrollStateManipulator(BodyPositionFixed), // Fix body position + /* + Using `position: 'fixed'` to lock background scroll. + This approach is chosen over: + 1. `overflow: 'hidden'`: It hides the scrollbar, causing layout "jumps". + `scrollbar-gutter` can fix it but it lacks Safari support and introduces + complexity of positioning calculations on modal. + 2. `overscrollBehavior`: Only stops scrolling at scroll limits, not suitable for all cases. + 3. `touchAction: none`: Ineffective on non-touch (desktop) devices. + */ + ...[ // Keep the scrollbar visible + createScrollStateManipulator(BodyStyleOverflowX), // Horizontal scrollbar + createScrollStateManipulator(BodyStyleOverflowY), // Vertical scrollbar + ], + ...[ // Fix scroll-to-top issue + // Horizontal + createScrollStateManipulator(HtmlScrollLeft), // Restore scroll position + createScrollStateManipulator(BodyStyleLeft), // Keep the body on scrolled position + // // Vertical + createScrollStateManipulator(HtmlScrollTop), // Restore scroll position + createScrollStateManipulator(BodyStyleTop), // Keep the body on scrolled position + ], + ...[ // Fix layout-shift on very large screens + // Using percentages instead of viewport allows content to grow if the content + // exceeds the viewport. + createScrollStateManipulator(BodyWidth100Percent), + createScrollStateManipulator(BodyHeight100Percent), + ], +] as const; + +enum ScrollRevertAction { + RestoreRequired, + SkipRestore, +} + +interface DomPropertyMutator { + storeInitialState(dom: ScrollDomStateAccessor): TInitialStateValue; + onBlock(storedState: TInitialStateValue, dom: ScrollDomStateAccessor): ScrollRevertAction; + restoreStateOnUnblock(storedState: TInitialStateValue, dom: ScrollDomStateAccessor): void; +} + +/** Calculates allocated scrollbar gutter, adjusting for `scrollbar-gutter: stable` */ +function calculateScrollbarGutterStyle( + clientSize: number, + offsetSize: number, +): string { + const scrollbarGutterSize = clientSize - offsetSize; + return scrollbarGutterSize !== 0 ? `${scrollbarGutterSize}px` : ''; +} + +function calculateAdjustedStyle( + spaceOffsets: readonly string[], +): string { + let value = '100%'; + const calculatedMargin = spaceOffsets + .filter((marginText) => marginText.length > 0) + .join(' + '); // without setting margins, it leads to layout shift if body has margin + if (calculatedMargin) { + value = `calc(${value} - (${calculatedMargin}))`; + } + return value; +} diff --git a/src/presentation/components/Shared/Modal/Hooks/ScrollLock/WindowScrollDomStateAccessor.ts b/src/presentation/components/Shared/Modal/Hooks/ScrollLock/WindowScrollDomStateAccessor.ts new file mode 100644 index 00000000..0b121b21 --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/ScrollLock/WindowScrollDomStateAccessor.ts @@ -0,0 +1,68 @@ +import type { ScrollDomStateAccessor } from './ScrollDomStateAccessor'; + +const HtmlElement = document.documentElement; +const BodyElement = document.body; + +export function getWindowDomState(): ScrollDomStateAccessor { + return new WindowScrollDomState(); +} + +class WindowScrollDomState implements ScrollDomStateAccessor { + get bodyStyleOverflowX(): string { return BodyElement.style.overflowX; } + + set bodyStyleOverflowX(value: string) { BodyElement.style.overflowX = value; } + + get bodyStyleOverflowY(): string { return BodyElement.style.overflowY; } + + set bodyStyleOverflowY(value: string) { BodyElement.style.overflowY = value; } + + get htmlScrollLeft(): number { return HtmlElement.scrollLeft; } + + set htmlScrollLeft(value: number) { HtmlElement.scrollLeft = value; } + + get htmlScrollTop(): number { return HtmlElement.scrollTop; } + + set htmlScrollTop(value: number) { HtmlElement.scrollTop = value; } + + get bodyStyleLeft(): string { return BodyElement.style.left; } + + set bodyStyleLeft(value: string) { BodyElement.style.left = value; } + + get bodyStyleTop(): string { return BodyElement.style.top; } + + set bodyStyleTop(value: string) { BodyElement.style.top = value; } + + get bodyStylePosition(): string { return BodyElement.style.position; } + + set bodyStylePosition(value: string) { BodyElement.style.position = value; } + + get bodyStyleWidth(): string { return BodyElement.style.width; } + + set bodyStyleWidth(value: string) { BodyElement.style.width = value; } + + get bodyStyleHeight(): string { return BodyElement.style.height; } + + set bodyStyleHeight(value: string) { BodyElement.style.height = value; } + + get bodyComputedMarginLeft(): string { return window.getComputedStyle(BodyElement).marginLeft; } + + get bodyComputedMarginRight(): string { return window.getComputedStyle(BodyElement).marginRight; } + + get bodyComputedMarginTop(): string { return window.getComputedStyle(BodyElement).marginTop; } + + get bodyComputedMarginBottom(): string { + return window.getComputedStyle(BodyElement).marginBottom; + } + + get htmlScrollWidth(): number { return HtmlElement.scrollWidth; } + + get htmlScrollHeight(): number { return HtmlElement.scrollHeight; } + + get htmlClientWidth(): number { return HtmlElement.clientWidth; } + + get htmlClientHeight(): number { return HtmlElement.clientHeight; } + + get htmlOffsetWidth(): number { return HtmlElement.offsetWidth; } + + get htmlOffsetHeight(): number { return HtmlElement.offsetHeight; } +} diff --git a/src/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.ts b/src/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.ts new file mode 100644 index 00000000..e73d003f --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.ts @@ -0,0 +1,37 @@ +import { type Ref, computed, watch } from 'vue'; + +/** + * This function monitors a set of conditions (represented as refs) and + * maintains a composite status based on all conditions. + */ +export function useAllTrueWatcher( + ...conditions: Ref[] +) { + const allMetCallbacks = new Array<() => void>(); + + const areAllConditionsMet = computed(() => conditions.every((condition) => condition.value)); + + watch(areAllConditionsMet, (areMet) => { + if (areMet) { + allMetCallbacks.forEach((action) => action()); + } + }); + + function resetAllConditions() { + conditions.forEach((condition) => { + condition.value = false; + }); + } + + function onAllConditionsMet(callback: () => void) { + allMetCallbacks.push(callback); + if (areAllConditionsMet.value) { + callback(); + } + } + + return { + resetAllConditions, + onAllConditionsMet, + }; +} diff --git a/src/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.ts b/src/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.ts new file mode 100644 index 00000000..9dea222b --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.ts @@ -0,0 +1,23 @@ +import { type Ref, watchEffect } from 'vue'; + +/** + * Manages focus transitions, ensuring good usability and accessibility. + */ +export function useCurrentFocusToggle(shouldDisableFocus: Ref) { + let previouslyFocusedElement: HTMLElement | null; + + watchEffect(() => { + if (shouldDisableFocus.value) { + previouslyFocusedElement = document.activeElement as HTMLElement | null; + previouslyFocusedElement?.blur(); + } else { + if (!previouslyFocusedElement || previouslyFocusedElement.tagName === 'BODY') { + // It doesn't make sense to return focus to the body after the modal is + // closed because the body itself doesn't offer meaningful interactivity + return; + } + previouslyFocusedElement.focus(); + previouslyFocusedElement = null; + } + }); +} diff --git a/src/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.ts b/src/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.ts new file mode 100644 index 00000000..e7320013 --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.ts @@ -0,0 +1,14 @@ +import { useAutoUnsubscribedEventListener, type UseEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener'; + +export function useEscapeKeyListener( + callback: () => void, + eventTarget: EventTarget = document, + useEventListener: UseEventListener = useAutoUnsubscribedEventListener, +): void { + const { startListening } = useEventListener(); + startListening(eventTarget, 'keyup', (event) => { + if (event.key === 'Escape') { + callback(); + } + }); +} diff --git a/src/presentation/components/Shared/Modal/ModalContainer.vue b/src/presentation/components/Shared/Modal/ModalContainer.vue new file mode 100644 index 00000000..13a3d805 --- /dev/null +++ b/src/presentation/components/Shared/Modal/ModalContainer.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/src/presentation/components/Shared/Modal/ModalContent.vue b/src/presentation/components/Shared/Modal/ModalContent.vue new file mode 100644 index 00000000..67c8f14c --- /dev/null +++ b/src/presentation/components/Shared/Modal/ModalContent.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/src/presentation/components/Shared/Modal/ModalDialog.vue b/src/presentation/components/Shared/Modal/ModalDialog.vue new file mode 100644 index 00000000..12f32924 --- /dev/null +++ b/src/presentation/components/Shared/Modal/ModalDialog.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/presentation/components/Shared/Modal/ModalOverlay.vue b/src/presentation/components/Shared/Modal/ModalOverlay.vue new file mode 100644 index 00000000..2834a15c --- /dev/null +++ b/src/presentation/components/Shared/Modal/ModalOverlay.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/src/presentation/components/Shared/OperatingSystemNames.ts b/src/presentation/components/Shared/OperatingSystemNames.ts new file mode 100644 index 00000000..f7e9c2a5 --- /dev/null +++ b/src/presentation/components/Shared/OperatingSystemNames.ts @@ -0,0 +1,15 @@ +import { OperatingSystem } from '@/domain/OperatingSystem'; + +export function getOperatingSystemDisplayName(os: OperatingSystem): string { + const displayName = OperatingSystemNames[os]; + if (!displayName) { + throw new RangeError(`Unsupported operating system ID: ${os}`); + } + return displayName; +} + +const OperatingSystemNames: Partial> = { + [OperatingSystem.Windows]: 'Windows', + [OperatingSystem.macOS]: 'macOS', + [OperatingSystem.Linux]: 'Linux', +}; diff --git a/src/presentation/components/Shared/SizeObserver.vue b/src/presentation/components/Shared/SizeObserver.vue new file mode 100644 index 00000000..ef4eeae6 --- /dev/null +++ b/src/presentation/components/Shared/SizeObserver.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/presentation/components/Shared/TooltipWrapper.vue b/src/presentation/components/Shared/TooltipWrapper.vue new file mode 100644 index 00000000..2c89e354 --- /dev/null +++ b/src/presentation/components/Shared/TooltipWrapper.vue @@ -0,0 +1,280 @@ + + + + + diff --git a/src/presentation/components/TheFooter/DownloadUrlList.vue b/src/presentation/components/TheFooter/DownloadUrlList.vue new file mode 100644 index 00000000..c1176bb4 --- /dev/null +++ b/src/presentation/components/TheFooter/DownloadUrlList.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/presentation/components/TheFooter/DownloadUrlListItem.vue b/src/presentation/components/TheFooter/DownloadUrlListItem.vue new file mode 100644 index 00000000..ccdb79cc --- /dev/null +++ b/src/presentation/components/TheFooter/DownloadUrlListItem.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/presentation/components/TheFooter/PrivacyPolicy.vue b/src/presentation/components/TheFooter/PrivacyPolicy.vue new file mode 100644 index 00000000..9030b042 --- /dev/null +++ b/src/presentation/components/TheFooter/PrivacyPolicy.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/src/presentation/components/TheFooter/TheFooter.vue b/src/presentation/components/TheFooter/TheFooter.vue new file mode 100644 index 00000000..36b96c75 --- /dev/null +++ b/src/presentation/components/TheFooter/TheFooter.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/presentation/components/TheHeader.vue b/src/presentation/components/TheHeader.vue new file mode 100644 index 00000000..581df408 --- /dev/null +++ b/src/presentation/components/TheHeader.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/presentation/components/TheSearchBar.vue b/src/presentation/components/TheSearchBar.vue new file mode 100644 index 00000000..e342541b --- /dev/null +++ b/src/presentation/components/TheSearchBar.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/src/presentation/electron/build/README.md b/src/presentation/electron/build/README.md new file mode 100644 index 00000000..ca8a1ce1 --- /dev/null +++ b/src/presentation/electron/build/README.md @@ -0,0 +1,13 @@ +# build + +This folder is the *build resources directory* [1] and contains files used by electron-build to +create the desktop version of the application. + +Generate icons from the main logo file. +Do not modify these icons manually. +For more details, see the [related documentation](./../../../../img/README.md). + +The electron-builder [1] uses these files, as specified in the [configuration file](./../../../../electron-builder.cjs). + +For more information on icons in electron-builder, +visit [Icons - electron-builder | www.electron.build](https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html). diff --git a/src/presentation/electron/build/icon.ico b/src/presentation/electron/build/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d0389d8fc219c6216bc1da1c6fa601af4305a463 GIT binary patch literal 372526 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 KFM&O^8B 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 literal 0 HcmV?d00001 diff --git a/src/presentation/electron/build/icon.png b/src/presentation/electron/build/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..89cee350064e922896c76a6f55200088ead5fde9 GIT binary patch literal 51985 zcmeEu`8(9#8~1w_Gxjx-9@=x#1{B7$;)5Cn-3?Fl3Z zLW3XC5I+X|K~dDX0zp)WNZ9KU^LcJGy5Z=7LF%IEiyPv5u6~V^R!!WVMBT1}L?&hb zIQjDO3$+VdbS~%)UsEY5DJj46Ub=MUYnSswp z%;JJB5z;;UPRys{WO(|*>nCHHqg~jfYip5K;>l}`@cQ`E<1d*F9PT+X#=v~lI348* zJxyBZgKq1#GR5-_uK&B%PU-wk!x4jZr65Mu;o6!T-YE}~R|o0s>%nkS+vNl`g9^@K ze|w)ZEC0aykMS?(BEF&>B?Xq~2Y5)eiv~sZ;yrw~k@K38Xd@c-BK{)VtPdwKMe@+d z+kO*AJpaD5>n<+HU6-Ak^*J3cGdDq7rW#YvQ5w9fKVasxCzEcHLrxo6fYK!+DZ(f29)cR#V?NK*Cz)PJJxw67tV#k{Cal}zYp&uO0#3p} z_UAhhDi!@n?OeAK1TxwpAKcawiuvT+e9EIrl*HN$Z8{u?a! zk|*R^B7(>m5X3_`<9$!6th2m(s)f#A&g|FyYpO2Cew1YXulqrP_aFV!7BTYSI^pW> zyV>II&NcjDT0ryh$B{OqU$~(e5%u10u4>&Y^*UAs5|e9Hl3okMp!)9)uW!)Isq%@X z@S`@v%cltkXmOjM~{m{EZ4B@Re~cqj(lUhct}$c;aB!x&*As}ti9vIun1hI zTePL7nu;UF-U2$!ejgZ3F&WbRrTc@g-8siTr10gs7c~_fSI!FT{{kD)y1&O;>#!qT0!HuO4DP{(A?;i`!bHBq7PVk~#<&#gaC1L+d$3#Ke z7y4JEk}UYRw2bcpOPialQ&LL2a74wENSpV98p+�*X0}&_`ZSfBZ|H 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 F{T~vkW~u-H literal 0 HcmV?d00001 diff --git a/src/presentation/electron/main/ElectronConfig.ts b/src/presentation/electron/main/ElectronConfig.ts new file mode 100644 index 00000000..2954f64f --- /dev/null +++ b/src/presentation/electron/main/ElectronConfig.ts @@ -0,0 +1,16 @@ +/** + * Abstraction for electron-vite specific logic and other Electron CLI helpers/wrappers. + * Allows for agnostic application design and centralizes adjustments when switching wrappers. + */ + +/// +import { join } from 'node:path'; +import appIcon from '@/presentation/public/icon.png?asset'; + +export const APP_ICON_PATH = appIcon; + +export const RENDERER_URL = process.env.ELECTRON_RENDERER_URL; + +export const RENDERER_HTML_PATH = join('file://', __dirname, '../renderer/index.html'); + +export const PRELOADER_SCRIPT_PATH = join(__dirname, '../preload/index.mjs'); diff --git a/src/presentation/electron/main/IpcRegistration.ts b/src/presentation/electron/main/IpcRegistration.ts new file mode 100644 index 00000000..490a87d1 --- /dev/null +++ b/src/presentation/electron/main/IpcRegistration.ts @@ -0,0 +1,44 @@ +import { ScriptFileCodeRunner } from '@/infrastructure/CodeRunner/ScriptFileCodeRunner'; +import type { CodeRunner } from '@/application/CodeRunner/CodeRunner'; +import type { Dialog } from '@/presentation/common/Dialog'; +import { ElectronDialog } from '@/infrastructure/Dialog/Electron/ElectronDialog'; +import type { IpcChannel } from '@/presentation/electron/shared/IpcBridging/IpcChannel'; +import { ScriptEnvironmentDiagnosticsCollector } from '@/infrastructure/ScriptDiagnostics/ScriptEnvironmentDiagnosticsCollector'; +import type { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector'; +import { registerIpcChannel } from '../shared/IpcBridging/IpcProxy'; +import { type ChannelDefinitionKey, IpcChannelDefinitions } from '../shared/IpcBridging/IpcChannelDefinitions'; + +export function registerAllIpcChannels( + registrar: IpcChannelRegistrar = registerIpcChannel, + createCodeRunner: CodeRunnerFactory = () => new ScriptFileCodeRunner(), + createDialog: DialogFactory = () => new ElectronDialog(), + createScriptDiagnosticsCollector + : ScriptDiagnosticsCollectorFactory = () => new ScriptEnvironmentDiagnosticsCollector(), +) { + const ipcInstanceCreators: IpcChannelRegistrars = { + CodeRunner: () => createCodeRunner(), + Dialog: () => createDialog(), + ScriptDiagnosticsCollector: () => createScriptDiagnosticsCollector(), + }; + Object.entries(ipcInstanceCreators).forEach(([name, instanceFactory]) => { + try { + const definition = IpcChannelDefinitions[name]; + const instance = instanceFactory(); + registrar(definition, instance); + } catch (err) { + throw new AggregateError([err], `main: Failed to register IPC channel "${name}":\n${err.message}`); + } + }); +} + +export type IpcChannelRegistrar = typeof registerIpcChannel; + +export type CodeRunnerFactory = () => CodeRunner; +export type DialogFactory = () => Dialog; +export type ScriptDiagnosticsCollectorFactory = () => ScriptDiagnosticsCollector; + +type RegistrationChannel = (typeof IpcChannelDefinitions)[T]; +type ExtractChannelServiceType = T extends IpcChannel ? U : never; +type IpcChannelRegistrars = { + [K in ChannelDefinitionKey]: () => ExtractChannelServiceType>; +}; diff --git a/src/presentation/electron/main/Update/AutomaticUpdateCoordinator.ts b/src/presentation/electron/main/Update/AutomaticUpdateCoordinator.ts new file mode 100644 index 00000000..c235f871 --- /dev/null +++ b/src/presentation/electron/main/Update/AutomaticUpdateCoordinator.ts @@ -0,0 +1,76 @@ +import { app, dialog } from 'electron/main'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { UpdateProgressBar } from './UpdateProgressBar'; +import { getAutoUpdater } from './ElectronAutoUpdaterFactory'; +import type { AppUpdater, UpdateInfo } from 'electron-updater'; +import type { ProgressInfo } from 'electron-builder'; + +export async function handleAutoUpdate() { + const autoUpdater = getAutoUpdater(); + if (await askDownloadAndInstall() === DownloadDialogResult.NotNow) { + return; + } + startHandlingUpdateProgress(autoUpdater); + await autoUpdater.downloadUpdate(); +} + +function startHandlingUpdateProgress(autoUpdater: AppUpdater) { + const progressBar = new UpdateProgressBar(); + progressBar.showIndeterminateState(); + autoUpdater.on('error', (e) => { + progressBar.showError(e); + }); + autoUpdater.on('download-progress', (progress: ProgressInfo) => { + /* + On macOS, download-progress event is not called. + So the indeterminate progress will continue until download is finished. + */ + ElectronLogger.debug('@download-progress@\n', progress); + progressBar.showProgress(progress); + }); + autoUpdater.on('update-downloaded', async (info: UpdateInfo) => { + ElectronLogger.info('@update-downloaded@\n', info); + progressBar.close(); + await handleUpdateDownloaded(autoUpdater); + }); +} + +async function handleUpdateDownloaded(autoUpdater: AppUpdater) { + if (await askRestartAndInstall() === InstallDialogResult.NotNow) { + return; + } + setTimeout(() => autoUpdater.quitAndInstall(), 1); +} + +enum DownloadDialogResult { + Install = 0, + NotNow = 1, +} +async function askDownloadAndInstall(): Promise { + const updateDialogResult = await dialog.showMessageBox({ + type: 'question', + buttons: ['Install', 'Not now'], + title: 'Confirm Update', + message: 'Update available.\n\nWould you like to download and install new version?', + detail: 'Application will automatically restart to apply update after download', + defaultId: DownloadDialogResult.Install, + cancelId: DownloadDialogResult.NotNow, + }); + return updateDialogResult.response; +} + +enum InstallDialogResult { + InstallAndRestart = 0, + NotNow = 1, +} +async function askRestartAndInstall(): Promise { + const installDialogResult = await dialog.showMessageBox({ + type: 'question', + buttons: ['Install and restart', 'Later'], + message: `A new version of ${app.name} has been downloaded.`, + detail: 'It will be installed the next time you restart the application.', + defaultId: InstallDialogResult.InstallAndRestart, + cancelId: InstallDialogResult.NotNow, + }); + return installDialogResult.response; +} diff --git a/src/presentation/electron/main/Update/ElectronAutoUpdaterFactory.ts b/src/presentation/electron/main/Update/ElectronAutoUpdaterFactory.ts new file mode 100644 index 00000000..7acbcd1c --- /dev/null +++ b/src/presentation/electron/main/Update/ElectronAutoUpdaterFactory.ts @@ -0,0 +1,8 @@ +import electronUpdater, { type AppUpdater } from 'electron-updater'; + +export function getAutoUpdater(): AppUpdater { + // Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'. + // It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976. + const { autoUpdater } = electronUpdater; + return autoUpdater; +} diff --git a/src/presentation/electron/main/Update/ManualUpdater/Dialogs.ts b/src/presentation/electron/main/Update/ManualUpdater/Dialogs.ts new file mode 100644 index 00000000..c4cd1e50 --- /dev/null +++ b/src/presentation/electron/main/Update/ManualUpdater/Dialogs.ts @@ -0,0 +1,122 @@ +import { dialog } from 'electron/main'; + +export enum ManualUpdateChoice { + NoAction = 0, + UpdateNow = 1, + VisitReleasesPage = 2, +} +export async function promptForManualUpdate(): Promise { + const visitPageResult = await dialog.showMessageBox({ + type: 'info', + buttons: [ + 'Not Now', + 'Download Update', + 'Visit Release Page', + ], + message: [ + 'A new version is available.', + 'Would you like to download it now?', + ].join('\n\n'), + detail: [ + 'Updates are highly recommended because they improve your privacy, security and safety.', + '\n\n', + 'Auto-updates are not fully supported on macOS due to code signing costs.', + 'Consider donating ❤️.', + ].join(' '), + defaultId: ManualUpdateChoice.UpdateNow, + cancelId: ManualUpdateChoice.NoAction, + }); + return visitPageResult.response; +} + +export enum IntegrityCheckChoice { + Cancel = 0, + RetryDownload = 1, + ContinueAnyway = 2, +} + +export async function promptIntegrityCheckFailure(): Promise { + const integrityResult = await dialog.showMessageBox({ + type: 'error', + buttons: [ + 'Cancel Update', + 'Retry Download', + 'Continue Anyway', + ], + message: 'Integrity check failed', + detail: + 'The integrity check for the installer has failed,' + + ' which means the file may be corrupted or has been tampered with.' + + ' It is recommended to retry the download or cancel the installation for your safety.' + + '\n\nContinuing the installation might put your system at risk.', + defaultId: IntegrityCheckChoice.RetryDownload, + cancelId: IntegrityCheckChoice.Cancel, + noLink: true, + }); + return integrityResult.response; +} + +export enum InstallerErrorChoice { + Cancel = 0, + RetryDownload = 1, + RetryOpen = 2, +} + +export async function promptInstallerOpenError(): Promise { + const result = await dialog.showMessageBox({ + type: 'error', + buttons: [ + 'Cancel Update', + 'Retry Download', + 'Retry Installation', + ], + message: 'Installation Error', + detail: 'The installer could not be launched. Please try again.', + defaultId: InstallerErrorChoice.RetryOpen, + cancelId: InstallerErrorChoice.Cancel, + noLink: true, + }); + return result.response; +} + +export enum DownloadErrorChoice { + Cancel = 0, + RetryDownload = 1, +} + +export async function promptDownloadError(): Promise { + const result = await dialog.showMessageBox({ + type: 'error', + buttons: [ + 'Cancel Update', + 'Retry Download', + ], + message: 'Download Error', + detail: 'Unable to download the update. Check your internet connection or try again later.', + defaultId: DownloadErrorChoice.RetryDownload, + cancelId: DownloadErrorChoice.Cancel, + noLink: true, + }); + return result.response; +} + +export enum UnexpectedErrorChoice { + Cancel = 0, + RetryUpdate = 1, +} + +export async function showUnexpectedError(): Promise { + const result = await dialog.showMessageBox({ + type: 'error', + buttons: [ + 'Cancel', + 'Retry Update', + ], + message: 'Unexpected Error', + detail: 'An unexpected error occurred. Would you like to retry updating?', + defaultId: UnexpectedErrorChoice.RetryUpdate, + cancelId: UnexpectedErrorChoice.Cancel, + noLink: true, + }); + return result.response; +} diff --git a/src/presentation/electron/main/Update/ManualUpdater/Downloader.ts b/src/presentation/electron/main/Update/ManualUpdater/Downloader.ts new file mode 100644 index 00000000..a0356468 --- /dev/null +++ b/src/presentation/electron/main/Update/ManualUpdater/Downloader.ts @@ -0,0 +1,222 @@ +import { existsSync, createWriteStream, type WriteStream } from 'node:fs'; +import { unlink, mkdir } from 'node:fs/promises'; +import path from 'node:path'; +import { app } from 'electron/main'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { UpdateProgressBar } from '../UpdateProgressBar'; +import { retryFileSystemAccess } from './RetryFileSystemAccess'; +import type { UpdateInfo } from 'electron-updater'; +import type { ReadableStream } from 'node:stream/web'; + +const MAX_PROGRESS_LOG_ENTRIES = 10; +const UNKNOWN_SIZE_LOG_INTERVAL_BYTES = 10 * 1024 * 1024; // 10 MB + +export type DownloadUpdateResult = { + readonly success: false; +} | { + readonly success: true; + readonly installerPath: string; +}; + +export async function downloadUpdate( + info: UpdateInfo, + remoteFileUrl: string, + progressBar: UpdateProgressBar, +): Promise { + ElectronLogger.info('Starting manual update download.'); + progressBar.showIndeterminateState(); + try { + const { filePath } = await downloadInstallerFile( + info.version, + remoteFileUrl, + (percentage) => { progressBar.showPercentage(percentage); }, + ); + return { + success: true, + installerPath: filePath, + }; + } catch (e) { + progressBar.showError(e); + return { + success: false, + }; + } +} + +async function downloadInstallerFile( + version: string, + remoteFileUrl: string, + progressHandler: ProgressCallback, +): Promise<{ readonly filePath: string; }> { + const filePath = `${path.dirname(app.getPath('temp'))}/privacy.sexy/${version}-installer.dmg`; + if (!await ensureFilePathReady(filePath)) { + throw new Error(`Failed to prepare the file path for the installer: ${filePath}`); + } + await downloadFileWithProgress( + remoteFileUrl, + filePath, + progressHandler, + ); + return { filePath }; +} + +async function ensureFilePathReady(filePath: string): Promise { + return retryFileSystemAccess(async () => { + try { + const parentFolder = path.dirname(filePath); + if (existsSync(filePath)) { + ElectronLogger.info(`Existing update file found and will be replaced: ${filePath}`); + await unlink(filePath); + } else { + await mkdir(parentFolder, { recursive: true }); + } + return true; + } catch (error) { + ElectronLogger.error(`Failed to prepare file path for update: ${filePath}`, error); + return false; + } + }); +} + +type ProgressCallback = (progress: number) => void; + +async function downloadFileWithProgress( + url: string, + filePath: string, + progressHandler: ProgressCallback, +) { + // autoUpdater cannot handle DMG files, requiring manual download management for these file types. + ElectronLogger.info(`Retrieving update from ${url}.`); + const response = await fetch(url); + if (!response.ok) { + throw Error(`Download failed: Server responded with ${response.status} ${response.statusText}.`); + } + const contentLength = getContentLengthFromResponse(response); + await withWriteStream(filePath, async (writer) => { + ElectronLogger.info(contentLength.isValid + ? `Saving file to ${filePath} (Size: ${contentLength.totalLength} bytes).` + : `Saving file to ${filePath}.`); + await withReadableStream(response, async (reader) => { + await streamWithProgress(contentLength, reader, writer, progressHandler); + }); + }); +} + +type ResponseContentLength = { + readonly isValid: true; + readonly totalLength: number; +} | { + readonly isValid: false; +}; + +function getContentLengthFromResponse(response: Response): ResponseContentLength { + const contentLengthString = response.headers.get('content-length'); + const headersInfo = Array.from(response.headers.entries()); + if (!contentLengthString) { + ElectronLogger.warn('Missing \'Content-Length\' header in the response.', headersInfo); + return { isValid: false }; + } + const contentLength = Number(contentLengthString); + if (Number.isNaN(contentLength) || contentLength <= 0) { + ElectronLogger.error('Unable to determine download size from server response.', headersInfo); + return { isValid: false }; + } + return { totalLength: contentLength, isValid: true }; +} + +async function withReadableStream( + response: Response, + handler: (readStream: ReadableStream) => Promise, +) { + const reader = createReader(response); + try { + await handler(reader); + } finally { + reader.cancel(); + } +} + +async function withWriteStream( + filePath: string, + handler: (writeStream: WriteStream) => Promise, +) { + const writer = createWriteStream(filePath); + try { + await handler(writer); + } finally { + writer.end(); + } +} + +async function streamWithProgress( + contentLength: ResponseContentLength, + readStream: ReadableStream, + writeStream: WriteStream, + progressHandler: ProgressCallback, +): Promise { + let receivedLength = 0; + let logThreshold = 0; + for await (const chunk of readStream) { + if (!chunk) { + throw Error('Received empty data chunk during download.'); + } + writeStream.write(Buffer.from(chunk)); + receivedLength += chunk.length; + notifyProgress(contentLength, receivedLength, progressHandler); + const progressLog = logProgress(receivedLength, contentLength, logThreshold); + logThreshold = progressLog.nextLogThreshold; + } + ElectronLogger.info('Update download completed successfully.'); +} + +function logProgress( + receivedLength: number, + contentLength: ResponseContentLength, + logThreshold: number, +): { readonly nextLogThreshold: number; } { + const { + shouldLog, nextLogThreshold, + } = shouldLogProgress(receivedLength, contentLength, logThreshold); + if (shouldLog) { + ElectronLogger.debug(`Download progress: ${receivedLength} bytes received.`); + } + return { nextLogThreshold }; +} + +function notifyProgress( + contentLength: ResponseContentLength, + receivedLength: number, + progressHandler: ProgressCallback, +) { + if (!contentLength.isValid) { + return; + } + const percentage = Math.floor((receivedLength / contentLength.totalLength) * 100); + progressHandler(percentage); +} + +function shouldLogProgress( + receivedLength: number, + contentLength: ResponseContentLength, + previousLogThreshold: number, +): { shouldLog: boolean, nextLogThreshold: number } { + const logInterval = contentLength.isValid + ? Math.ceil(contentLength.totalLength / MAX_PROGRESS_LOG_ENTRIES) + : UNKNOWN_SIZE_LOG_INTERVAL_BYTES; + + if (receivedLength >= previousLogThreshold + logInterval) { + return { shouldLog: true, nextLogThreshold: previousLogThreshold + logInterval }; + } + return { shouldLog: false, nextLogThreshold: previousLogThreshold }; +} + +function createReader(response: Response): ReadableStream { + if (!response.body) { + throw new Error('Response body is empty, cannot proceed with download.'); + } + // TypeScript has removed the async iterator type definition for ReadableStream due to + // limited browser support. Node.js, however, supports async iterable streams, allowing + // type casting to function properly in this context. + // https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/65542#discussioncomment-6071004 + return response.body as ReadableStream; +} diff --git a/src/presentation/electron/main/Update/ManualUpdater/Installer.ts b/src/presentation/electron/main/Update/ManualUpdater/Installer.ts new file mode 100644 index 00000000..46a3e3bf --- /dev/null +++ b/src/presentation/electron/main/Update/ManualUpdater/Installer.ts @@ -0,0 +1,17 @@ +import { app } from 'electron/main'; +import { shell } from 'electron/common'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { retryFileSystemAccess } from './RetryFileSystemAccess'; + +export async function startInstallation(filePath: string): Promise { + return retryFileSystemAccess(async () => { + ElectronLogger.info(`Attempting to open the installer at: ${filePath}.`); + const error = await shell.openPath(filePath); + if (!error) { + app.quit(); + return true; + } + ElectronLogger.error(`Failed to open the installer at ${filePath}.`, error); + return false; + }); +} diff --git a/src/presentation/electron/main/Update/ManualUpdater/Integrity.ts b/src/presentation/electron/main/Update/ManualUpdater/Integrity.ts new file mode 100644 index 00000000..599eaf01 --- /dev/null +++ b/src/presentation/electron/main/Update/ManualUpdater/Integrity.ts @@ -0,0 +1,38 @@ +import { createHash } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { retryFileSystemAccess } from './RetryFileSystemAccess'; + +export async function checkIntegrity( + filePath: string, + base64Sha512: string, +): Promise { + return retryFileSystemAccess( + async () => { + const hash = await computeSha512(filePath); + if (hash === base64Sha512) { + ElectronLogger.info(`Integrity check passed for file: ${filePath}.`); + return true; + } + ElectronLogger.warn([ + `Integrity check failed for file: ${filePath}`, + `Expected hash: ${base64Sha512}, but found: ${hash}`, + ].join('\n')); + return false; + }, + ); +} + +async function computeSha512(filePath: string): Promise { + try { + const hash = createHash('sha512'); + const stream = createReadStream(filePath); + for await (const chunk of stream) { + hash.update(chunk); + } + return hash.digest('base64'); + } catch (error) { + ElectronLogger.error(`Failed to compute SHA512 hash for file: ${filePath}`, error); + throw error; // Rethrow to handle it in the calling context if necessary + } +} diff --git a/src/presentation/electron/main/Update/ManualUpdater/ManualUpdateCoordinator.ts b/src/presentation/electron/main/Update/ManualUpdater/ManualUpdateCoordinator.ts new file mode 100644 index 00000000..49c99913 --- /dev/null +++ b/src/presentation/electron/main/Update/ManualUpdater/ManualUpdateCoordinator.ts @@ -0,0 +1,154 @@ +import { shell } from 'electron'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { GitHubProjectDetails } from '@/domain/Project/GitHubProjectDetails'; +import { Version } from '@/domain/Version'; +import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { UpdateProgressBar } from '../UpdateProgressBar'; +import { + promptForManualUpdate, promptInstallerOpenError, + promptIntegrityCheckFailure, promptDownloadError, + DownloadErrorChoice, InstallerErrorChoice, IntegrityCheckChoice, + ManualUpdateChoice, showUnexpectedError, UnexpectedErrorChoice, +} from './Dialogs'; +import { type DownloadUpdateResult, downloadUpdate } from './Downloader'; +import { checkIntegrity } from './Integrity'; +import { startInstallation } from './Installer'; +import type { UpdateInfo } from 'electron-updater'; + +export function requiresManualUpdate(): boolean { + return process.platform === 'darwin'; +} + +export async function startManualUpdateProcess(info: UpdateInfo) { + try { + const updateAction = await promptForManualUpdate(); + if (updateAction === ManualUpdateChoice.NoAction) { + ElectronLogger.info('User cancelled the update.'); + return; + } + const { releaseUrl, downloadUrl } = getRemoteUpdateUrls(info.version); + if (updateAction === ManualUpdateChoice.VisitReleasesPage) { + ElectronLogger.info(`Navigating to release page: ${releaseUrl}`); + await shell.openExternal(releaseUrl); + } else if (updateAction === ManualUpdateChoice.UpdateNow) { + ElectronLogger.info('Initiating update download and installation.'); + await downloadAndInstallUpdate(downloadUrl, info); + } + } catch (err) { + ElectronLogger.error('Unexpected error during updates', err); + await handleUnexpectedError(info); + } +} + +async function downloadAndInstallUpdate(fileUrl: string, info: UpdateInfo) { + let download: DownloadUpdateResult | undefined; + await withProgressBar(async (progressBar) => { + download = await downloadUpdate(info, fileUrl, progressBar); + }); + if (!download?.success) { + await handleFailedDownload(info); + return; + } + if (await isIntegrityPreserved(download.installerPath, fileUrl, info)) { + await openInstaller(download.installerPath, info); + return; + } + const userAction = await promptIntegrityCheckFailure(); + if (userAction === IntegrityCheckChoice.RetryDownload) { + await startManualUpdateProcess(info); + } else if (userAction === IntegrityCheckChoice.ContinueAnyway) { + ElectronLogger.warn('Proceeding to install with failed integrity check.'); + await openInstaller(download.installerPath, info); + } +} + +async function handleFailedDownload(info: UpdateInfo) { + const userAction = await promptDownloadError(); + if (userAction === DownloadErrorChoice.Cancel) { + ElectronLogger.info('Update download canceled.'); + } else if (userAction === DownloadErrorChoice.RetryDownload) { + ElectronLogger.info('Retrying update download.'); + await startManualUpdateProcess(info); + } +} + +async function handleUnexpectedError(info: UpdateInfo) { + const userAction = await showUnexpectedError(); + if (userAction === UnexpectedErrorChoice.Cancel) { + ElectronLogger.info('Unexpected error handling canceled.'); + } else if (userAction === UnexpectedErrorChoice.RetryUpdate) { + ElectronLogger.info('Retrying the update process.'); + await startManualUpdateProcess(info); + } +} + +async function openInstaller(installerPath: string, info: UpdateInfo) { + if (await startInstallation(installerPath)) { + return; + } + const userAction = await promptInstallerOpenError(); + if (userAction === InstallerErrorChoice.RetryDownload) { + await startManualUpdateProcess(info); + } else if (userAction === InstallerErrorChoice.RetryOpen) { + await openInstaller(installerPath, info); + } +} + +async function withProgressBar( + action: (progressBar: UpdateProgressBar) => Promise, +) { + const progressBar = new UpdateProgressBar(); + await action(progressBar); + progressBar.close(); +} + +async function isIntegrityPreserved( + filePath: string, + fileUrl: string, + info: UpdateInfo, +): Promise { + const sha512Hash = getRemoteSha512Hash(info, fileUrl); + if (!sha512Hash) { + return false; + } + const integrityCheckResult = await checkIntegrity(filePath, sha512Hash); + return integrityCheckResult; +} + +function getRemoteSha512Hash(info: UpdateInfo, fileUrl: string): string | undefined { + const fileInfos = info.files.filter((file) => fileUrl.includes(file.url)); + if (!fileInfos.length) { + ElectronLogger.error(`Remote hash not found for the URL: ${fileUrl}`, info.files); + if (info.files.length > 0) { + const firstHash = info.files[0].sha512; + ElectronLogger.info(`Selecting the first available hash: ${firstHash}`); + return firstHash; + } + return undefined; + } + if (fileInfos.length > 1) { + ElectronLogger.error(`Found multiple file entries for the URL: ${fileUrl}`, fileInfos); + } + return fileInfos[0].sha512; +} + +interface UpdateUrls { + readonly releaseUrl: string; + readonly downloadUrl: string; +} + +function getRemoteUpdateUrls(targetVersion: string): UpdateUrls { + const existingProject = parseProjectDetails(); + const targetProject = new GitHubProjectDetails( + existingProject.name, + new Version(targetVersion), + existingProject.slogan, + existingProject.repositoryUrl, + existingProject.homepage, + ); + return { + releaseUrl: targetProject.releaseUrl, + downloadUrl: targetProject.getDownloadUrl(OperatingSystem.macOS), + }; +} diff --git a/src/presentation/electron/main/Update/ManualUpdater/RetryFileSystemAccess.ts b/src/presentation/electron/main/Update/ManualUpdater/RetryFileSystemAccess.ts new file mode 100644 index 00000000..d3be38df --- /dev/null +++ b/src/presentation/electron/main/Update/ManualUpdater/RetryFileSystemAccess.ts @@ -0,0 +1,39 @@ +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { sleep } from '@/infrastructure/Threading/AsyncSleep'; + +export function retryFileSystemAccess( + fileOperation: () => Promise, +): Promise { + return retryWithExponentialBackoff( + fileOperation, + TOTAL_RETRIES, + INITIAL_DELAY_MS, + ); +} + +// These values provide a balanced approach for handling transient file system +// issues without excessive waiting. +const INITIAL_DELAY_MS = 500; +const TOTAL_RETRIES = 3; + +async function retryWithExponentialBackoff( + operation: () => Promise, + maxAttempts: number, + delayInMs: number, + currentAttempt = 1, +): Promise { + const result = await operation(); + if (result || currentAttempt === maxAttempts) { + return result; + } + ElectronLogger.info(`Attempting retry (${currentAttempt}/${TOTAL_RETRIES}) in ${delayInMs} ms.`); + await sleep(delayInMs); + const exponentialDelayInMs = delayInMs * 2; + const nextAttempt = currentAttempt + 1; + return retryWithExponentialBackoff( + operation, + maxAttempts, + exponentialDelayInMs, + nextAttempt, + ); +} diff --git a/src/presentation/electron/main/Update/UpdateInitializer.ts b/src/presentation/electron/main/Update/UpdateInitializer.ts new file mode 100644 index 00000000..dc0e8c99 --- /dev/null +++ b/src/presentation/electron/main/Update/UpdateInitializer.ts @@ -0,0 +1,49 @@ +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { requiresManualUpdate, startManualUpdateProcess } from './ManualUpdater/ManualUpdateCoordinator'; +import { handleAutoUpdate } from './AutomaticUpdateCoordinator'; +import { getAutoUpdater } from './ElectronAutoUpdaterFactory'; +import type { UpdateInfo } from 'electron-updater'; + +interface Updater { + checkForUpdates(): Promise; +} + +export function setupAutoUpdater(): Updater { + const autoUpdater = getAutoUpdater(); + + autoUpdater.logger = ElectronLogger; + + // Auto-downloads are disabled to allow separate handling of 'check' and 'download' actions, + // which vary based on the specific platform and user preferences. + autoUpdater.autoDownload = false; + + autoUpdater.on('error', (error: Error) => { + ElectronLogger.error('@error@\n', error); + }); + + let isAlreadyHandled = false; + autoUpdater.on('update-available', async (info: UpdateInfo) => { + ElectronLogger.info('@update-available@\n', info); + if (isAlreadyHandled) { + ElectronLogger.info('Available updates is already handled'); + return; + } + isAlreadyHandled = true; + await handleAvailableUpdate(info); + }); + + return { + checkForUpdates: async () => { + // autoUpdater.emit('update-available'); // For testing + await autoUpdater.checkForUpdates(); + }, + }; +} + +async function handleAvailableUpdate(info: UpdateInfo) { + if (requiresManualUpdate()) { + await startManualUpdateProcess(info); + return; + } + await handleAutoUpdate(); +} diff --git a/src/presentation/electron/main/Update/UpdateProgressBar.ts b/src/presentation/electron/main/Update/UpdateProgressBar.ts new file mode 100644 index 00000000..9ee7c025 --- /dev/null +++ b/src/presentation/electron/main/Update/UpdateProgressBar.ts @@ -0,0 +1,95 @@ +import ProgressBar from 'electron-progressbar'; +import { app, BrowserWindow } from 'electron/main'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import type { ProgressInfo } from 'electron-builder'; + +export class UpdateProgressBar { + private progressBar: ProgressBar | undefined; + + private get innerProgressBarWindow(): BrowserWindow { + // eslint-disable-next-line no-underscore-dangle + return this.progressBar._window; + } + + private showingProgress = false; + + public showIndeterminateState() { + this.progressBar?.close(); + this.progressBar = progressBarFactory.createWithIndeterminateState(); + } + + public showProgress(progress: ProgressInfo) { + const percentage = getUpdatePercent(progress); + this.showPercentage(percentage); + } + + public showPercentage(percentage: number) { + if (!this.showingProgress) { // First time showing progress + this.progressBar?.close(); + this.showingProgress = true; + this.progressBar = progressBarFactory.createWithPercentile(percentage); + } else { + this.progressBar.value = percentage; + } + } + + public showError(e: Error) { + const reportUpdateError = () => { + this.progressBar.detail = 'An error occurred while fetching updates.' + + `\n${e && e.message ? e.message : e}`; + this.innerProgressBarWindow.setClosable(true); + }; + if (this.progressBar?.innerProgressBarWindow) { + reportUpdateError(); + } else { + this.progressBar?.on('ready', () => reportUpdateError()); + } + } + + public close() { + if (!this.progressBar?.isCompleted()) { + this.progressBar?.close(); + } + } +} + +function getUpdatePercent(progress: ProgressInfo) { + let { percent } = progress; + if (percent) { + percent = Math.round(percent * 100) / 100; + } + return percent; +} + +const progressBarFactory = { + createWithIndeterminateState: () => { + return new ProgressBar({ + title: `${app.name} Update`, + text: `Downloading ${app.name} update...`, + }); + }, + createWithPercentile: (initialPercentage: number) => { + const progressBar = new ProgressBar({ + indeterminate: false, + title: `${app.name} Update`, + text: `Downloading ${app.name} update...`, + detail: `${initialPercentage}% ...`, + initialValue: initialPercentage, + }); + progressBar + .on('completed', () => { + progressBar.detail = 'Download completed.'; + }) + .on('aborted', (value: number) => { + ElectronLogger.info(`Progress aborted... ${value}`); + }) + .on('progress', (value: number) => { + progressBar.detail = `${value}% ...`; + }) + .on('ready', () => { + // initialValue doesn't set the UI, so this is needed to render it correctly + progressBar.value = initialPercentage; + }); + return progressBar; + }, +}; diff --git a/src/presentation/electron/main/index.ts b/src/presentation/electron/main/index.ts new file mode 100644 index 00000000..bb6d79d4 --- /dev/null +++ b/src/presentation/electron/main/index.ts @@ -0,0 +1,194 @@ +// Initializes Electron's main process, always runs in the background, and manages the main window. + +import { + app, protocol, BrowserWindow, screen, +} from 'electron/main'; +import { shell } from 'electron/common'; +import log from 'electron-log/main'; +import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'; +import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { setupAutoUpdater } from './Update/UpdateInitializer'; +import { + APP_ICON_PATH, PRELOADER_SCRIPT_PATH, RENDERER_HTML_PATH, RENDERER_URL, +} from './ElectronConfig'; +import { registerAllIpcChannels } from './IpcRegistration'; + +const hideWindowUntilLoaded = true; +const openDevToolsOnDevelopment = true; +const isDevelopment = !app.isPackaged; + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let win: BrowserWindow | null; + +// Scheme must be registered before the app is ready +protocol.registerSchemesAsPrivileged([ + { scheme: 'app', privileges: { secure: true, standard: true } }, +]); + +setupLogger(); + +validateRuntimeSanity({ + // Metadata is used by manual updates. + validateEnvironmentVariables: true, + + // Environment is populated by the preload script and is in the renderer's context; + // it's not directly accessible from the main process. + validateWindowVariables: false, +}); + +function createWindow() { + // Create the browser window. + const size = getWindowSize(1650, 955); + win = new BrowserWindow({ + width: size.width, + height: size.height, + webPreferences: { + nodeIntegration: true, // disabling does not work with electron-vite, https://electron-vite.org/guide/dev.html#nodeintegration + contextIsolation: true, + preload: PRELOADER_SCRIPT_PATH, + }, + icon: APP_ICON_PATH, + show: !hideWindowUntilLoaded, + }); + focusAndShowOnceLoaded(win); + win.setMenuBarVisibility(false); + configureExternalsUrlsOpenBrowser(win); + loadApplication(win); + win.on('closed', () => { + win = null; + }); +} + +configureAppQuitBehavior(); +registerAllIpcChannels(); + +app.whenReady().then(async () => { + if (isDevelopment) { + try { + await installExtension(VUEJS_DEVTOOLS); + } catch (e) { + ElectronLogger.error('Vue Devtools failed to install:', e.toString()); + } + } + createWindow(); + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + createWindow(); + } + }); +}); + +// Exit cleanly on request from parent process in development mode. +if (isDevelopment) { + if (process.platform === 'win32') { + process.on('message', (data) => { + if (data === 'graceful-exit') { + app.quit(); + } + }); + } else { + process.on('SIGTERM', () => { + app.quit(); + }); + } +} + +function loadApplication(window: BrowserWindow) { + if (RENDERER_URL) { // Populated in a dev server during development + loadUrlWithNodeWorkaround(window, RENDERER_URL); + } else { + loadUrlWithNodeWorkaround(window, RENDERER_HTML_PATH); + } + openDevTools(window); + if (!isDevelopment) { + const updater = setupAutoUpdater(); + updater.checkForUpdates(); + } + // Do not remove [WINDOW_INIT]; it's a marker used in tests. + ElectronLogger.info('[WINDOW_INIT] Main window initialized and content loading.'); +} + +function configureExternalsUrlsOpenBrowser(window: BrowserWindow) { + window.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: 'deny' }; + }); +} + +// Workaround for https://github.com/electron/electron/issues/19554 otherwise fs does not work +function loadUrlWithNodeWorkaround(window: BrowserWindow, url: string) { + setTimeout(() => { + window.loadURL(url); + }, 10); +} + +function getWindowSize(idealWidth: number, idealHeight: number) { + let { width, height } = screen.getPrimaryDisplay().workAreaSize; + // To ensure not creating a screen bigger than current screen size + // Not using "enableLargerThanScreen" as it's macOS only (see https://www.electronjs.org/docs/api/browser-window) + width = Math.min(width, idealWidth); + height = Math.min(height, idealHeight); + return { width, height }; +} + +function setupLogger(): void { + // log.initialize(); ← We inject logger to renderer through preloader, this is not needed. + log.transports.file.level = 'silly'; + log.eventLogger.startLogging(); +} + +function configureAppQuitBehavior() { + let macOsQuit = false; + // Quit when all windows are closed. + app.on('window-all-closed', () => { + if (process.platform === 'darwin' + && !macOsQuit) { + /* + On macOS it is common for applications and their menu bar + to stay active until the user quits explicitly with Cmd + Q + */ + return; + } + app.quit(); + }); + if (process.platform === 'darwin') { + /* + On macOS we application quit is stopped if user does not Cmd + Q + But we still want to be able to use app.quit() and quit the application + on menu bar, after updates etc. + */ + app.on('before-quit', () => { + macOsQuit = true; + }); + } +} + +function focusAndShowOnceLoaded(window: BrowserWindow) { + window.once('ready-to-show', () => { + window.show(); // Shows and focuses + bringToFront(window); + }); +} + +function bringToFront(window: BrowserWindow) { + // Only needed for Windows, tested on GNOME 42.5, Windows 11 23H2 Pro and macOS Sonoma 14.4.1. + // Some report it's also needed for some versions of GNOME. + // - https://github.com/electron/electron/issues/2867#issuecomment-409858459 + // - https://github.com/signalapp/Signal-Desktop/blob/0999df2d6e93da805b2135f788ffc739ba69832d/app/SystemTrayService.ts#L277-L284 + window.setAlwaysOnTop(true); + window.setAlwaysOnTop(false); +} + +function openDevTools(window: BrowserWindow) { + if (!isDevelopment) { + return; + } + if (!openDevToolsOnDevelopment) { + return; + } + window.webContents.openDevTools(); +} diff --git a/src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts b/src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts new file mode 100644 index 00000000..81613bff --- /dev/null +++ b/src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts @@ -0,0 +1,17 @@ +import { contextBridge } from 'electron/renderer'; +import { bindObjectMethods } from './MethodContextBinder'; +import { provideWindowVariables } from './RendererApiProvider'; + +export function connectApisWithContextBridge( + bridgeConnector: BridgeConnector = contextBridge.exposeInMainWorld, + apiObject: object = provideWindowVariables(), + methodContextBinder: MethodContextBinder = bindObjectMethods, +) { + Object.entries(apiObject).forEach(([key, value]) => { + bridgeConnector(key, methodContextBinder(value)); + }); +} + +export type BridgeConnector = typeof contextBridge.exposeInMainWorld; + +export type MethodContextBinder = typeof bindObjectMethods; diff --git a/src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts b/src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts new file mode 100644 index 00000000..fb530774 --- /dev/null +++ b/src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts @@ -0,0 +1,52 @@ +import { + isArray, isFunction, isNullOrUndefined, isPlainObject, +} from '@/TypeHelpers'; + +/** + * Binds method contexts to their original object instances and recursively processes + * nested objects and arrays. This is particularly useful when exposing objects across + * different contexts in Electron, such as from the main process to the renderer process + * via the `contextBridge`. + * + * In Electron's context isolation environment, methods of objects passed through the + * `contextBridge` lose their original context (`this` binding). This function ensures that + * each method retains its binding to its original object, allowing it to work as intended + * when invoked from the renderer process. + * + * This approach decouples context isolation concerns from class implementations, enabling + * classes to operate normally without needing explicit binding or arrow functions to maintain + * the context. + */ +export function bindObjectMethods(obj: T): T { + if (isNullOrUndefined(obj)) { + return obj; + } + if (isPlainObject(obj)) { + bindMethodsOfObject(obj); + Object.values(obj).forEach((value) => { + if (!isNullOrUndefined(value) && !isFunction(value)) { + bindObjectMethods(value); + } + }); + } else if (isArray(obj)) { + obj.forEach((item) => bindObjectMethods(item)); + } + return obj; +} + +function bindMethodsOfObject(obj: T): T { + const prototype = Object.getPrototypeOf(obj); + if (!prototype) { + return obj; + } + Object.getOwnPropertyNames(prototype).forEach((property) => { + if (!prototype.hasOwnProperty.call(obj, property)) { + return; // Skip properties not directly on the prototype + } + const value = obj[property]; + if (isFunction(value)) { + (obj as object)[property] = value.bind(obj); + } + }); + return obj; +} diff --git a/src/presentation/electron/preload/ContextBridging/README.md b/src/presentation/electron/preload/ContextBridging/README.md new file mode 100644 index 00000000..fabf29fa --- /dev/null +++ b/src/presentation/electron/preload/ContextBridging/README.md @@ -0,0 +1,14 @@ +# Context Bridging Module + +This module establishes secure, maintainable, and efficient inter-process communication between the preloader +and renderer processes. + +## Benefits + +- **Security**: Exposes intended parts of an object to the renderer process, safeguarding the application's + integrity and security. +- **Type Safety and Maintainability**: Offers type-checked contracts for robust and easy-to-maintain code. +- **Simplicity**: Streamlines the process of exposing APIs to the renderer process, minimizing the complexity + of context bindings. +- **Scalability**: Enhances the scalability of API exposure and simplifies managing more complex API structures, + overcoming the limitations of ad-hoc approaches. diff --git a/src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts b/src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts new file mode 100644 index 00000000..ff4a6fb1 --- /dev/null +++ b/src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts @@ -0,0 +1,36 @@ +import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import type { Logger } from '@/application/Common/Log/Logger'; +import type { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; +import { convertPlatformToOs } from '@/infrastructure/RuntimeEnvironment/Node/NodeOsMapper'; +import { createIpcConsumerProxy } from '../../shared/IpcBridging/IpcProxy'; +import { IpcChannelDefinitions } from '../../shared/IpcBridging/IpcChannelDefinitions'; +import { createSecureFacade } from './SecureFacadeCreator'; + +export function provideWindowVariables( + createApiFacade: ApiFacadeFactory = createSecureFacade, + ipcConsumerCreator: IpcConsumerProxyCreator = createIpcConsumerProxy, + convertToOs = convertPlatformToOs, + createLogger: LoggerFactory = () => createElectronLogger(), +): WindowVariables { + // Enforces mandatory variable availability at compile time + const variables: RequiredWindowVariables = { + isRunningAsDesktopApplication: true, + log: createApiFacade(createLogger(), ['info', 'debug', 'warn', 'error']), + os: convertToOs(process.platform), + codeRunner: ipcConsumerCreator(IpcChannelDefinitions.CodeRunner), + dialog: ipcConsumerCreator(IpcChannelDefinitions.Dialog), + scriptDiagnosticsCollector: ipcConsumerCreator( + IpcChannelDefinitions.ScriptDiagnosticsCollector, + ), + }; + return variables; +} + +type RequiredWindowVariables = PartiallyRequired; +type PartiallyRequired = Required> & Pick; + +export type ApiFacadeFactory = typeof createSecureFacade; + +export type IpcConsumerProxyCreator = typeof createIpcConsumerProxy; + +export type LoggerFactory = () => Logger; diff --git a/src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts b/src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts new file mode 100644 index 00000000..809e4099 --- /dev/null +++ b/src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts @@ -0,0 +1,42 @@ +import { isFunction } from '@/TypeHelpers'; + +/** + * Creates a secure proxy for the specified object, exposing only the public properties + * of its interface. + * + * This approach prevents the full exposure of the object, thereby reducing the risk + * of unintended access or misuse. For instance, creating a facade for a class rather + * than exposing the class itself ensures that private members and dependencies + * (such as file access or internal state) remain encapsulated and inaccessible. + */ +export function createSecureFacade( + originalObject: T, + accessibleMembers: KeyTypeCombinations, +): T { + const facade: Partial = {}; + + accessibleMembers.forEach((key: keyof T) => { + const member = originalObject[key]; + if (isFunction(member)) { + facade[key] = ((...args: unknown[]) => { + return member.apply(originalObject, args); + }) as T[keyof T]; + } else { + facade[key] = member; + } + }); + + return facade as T; +} + +type PrependTuple = H extends unknown ? T extends unknown ? + ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never : never : never; +type RecursionDepthControl = [ + never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, +]; +type AllKeyCombinations = T extends unknown ? + PrependTuple extends infer X ? { + 0: [], 1: AllKeyCombinations + }[[X] extends [never] ? 0 : 1] : never> : + never; +type KeyTypeCombinations = AllKeyCombinations; diff --git a/src/presentation/electron/preload/index.ts b/src/presentation/electron/preload/index.ts new file mode 100644 index 00000000..f4aeb82f --- /dev/null +++ b/src/presentation/electron/preload/index.ts @@ -0,0 +1,19 @@ +// This file is used to securely expose Electron APIs to the application. + +import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { connectApisWithContextBridge } from './ContextBridging/ApiContextBridge'; + +validateRuntimeSanity({ + // Validate metadata as a preventive measure for fail-fast, + // even if it's not currently used in the preload script. + validateEnvironmentVariables: true, + + // The preload script cannot access variables on the window object. + validateWindowVariables: false, +}); + +connectApisWithContextBridge(); + +// Do not remove [PRELOAD_INIT]; it's a marker used in tests. +ElectronLogger.info('[PRELOAD_INIT] Preload script successfully initialized and executed.'); diff --git a/src/presentation/electron/shared/IpcBridging/IpcChannel.ts b/src/presentation/electron/shared/IpcBridging/IpcChannel.ts new file mode 100644 index 00000000..1b7a3f5a --- /dev/null +++ b/src/presentation/electron/shared/IpcBridging/IpcChannel.ts @@ -0,0 +1,6 @@ +import type { FunctionKeys } from '@/TypeHelpers'; + +export interface IpcChannel { + readonly namespace: string; + readonly accessibleMembers: readonly FunctionKeys[]; // Property keys are not yet supported +} diff --git a/src/presentation/electron/shared/IpcBridging/IpcChannelDefinitions.ts b/src/presentation/electron/shared/IpcBridging/IpcChannelDefinitions.ts new file mode 100644 index 00000000..d517d357 --- /dev/null +++ b/src/presentation/electron/shared/IpcBridging/IpcChannelDefinitions.ts @@ -0,0 +1,23 @@ +import type { FunctionKeys } from '@/TypeHelpers'; +import type { CodeRunner } from '@/application/CodeRunner/CodeRunner'; +import type { Dialog } from '@/presentation/common/Dialog'; +import type { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector'; +import type { IpcChannel } from './IpcChannel'; + +export const IpcChannelDefinitions = { + CodeRunner: defineElectronIpcChannel('code-run', ['runCode']), + Dialog: defineElectronIpcChannel

('dialogs', ['showError', 'saveFile']), + ScriptDiagnosticsCollector: defineElectronIpcChannel('script-diagnostics-collector', ['collectDiagnosticInformation']), +} as const; + +export type ChannelDefinitionKey = keyof typeof IpcChannelDefinitions; + +function defineElectronIpcChannel( + name: string, + functionNames: readonly FunctionKeys[], +): IpcChannel { + return { + namespace: name, + accessibleMembers: functionNames, + }; +} diff --git a/src/presentation/electron/shared/IpcBridging/IpcProxy.ts b/src/presentation/electron/shared/IpcBridging/IpcProxy.ts new file mode 100644 index 00000000..64039fe2 --- /dev/null +++ b/src/presentation/electron/shared/IpcBridging/IpcProxy.ts @@ -0,0 +1,68 @@ +import { ipcMain } from 'electron/main'; +import { ipcRenderer } from 'electron/renderer'; +import { type FunctionKeys, isFunction } from '@/TypeHelpers'; +import type { IpcChannel } from './IpcChannel'; + +export function createIpcConsumerProxy( + channel: IpcChannel, + electronIpcRenderer: Electron.IpcRenderer = ipcRenderer, +): AsyncMethods { + const facade: Partial = {}; + channel.accessibleMembers.forEach((member) => { + const functionKey = member as string; + const ipcChannel = getIpcChannelIdentifier(channel.namespace, functionKey); + facade[functionKey] = ((...args: unknown[]) => { + return electronIpcRenderer.invoke(ipcChannel, ...args); + }) as AsyncMethods[keyof T]; + }); + return facade as AsyncMethods; +} + +export function registerIpcChannel( + channel: IpcChannel, + originalObject: T, + electronIpcMain: Electron.IpcMain = ipcMain, +) { + channel.accessibleMembers.forEach((functionKey) => { + const originalFunction = originalObject[functionKey]; + validateIpcFunction(functionKey, originalFunction, channel); + const ipcChannel = getIpcChannelIdentifier(channel.namespace, functionKey as string); + electronIpcMain.handle(ipcChannel, (_event, ...args: unknown[]) => { + return originalFunction.apply(originalObject, args); + }); + }); +} + +function validateIpcFunction( + functionKey: FunctionKeys, + functionValue: T[FunctionKeys], + channel: IpcChannel, +): asserts functionValue is T[FunctionKeys] & ((...args: unknown[]) => unknown) { + const functionName = functionKey.toString(); + if (functionValue === undefined) { + throwErrorWithContext(`The function "${functionName}" is not found on the target object.`); + } + if (!isFunction(functionValue)) { + throwErrorWithContext('Non-function members are not yet supported.'); + } + function throwErrorWithContext(message: string): never { + throw new Error([ + message, + `Channel: ${JSON.stringify(channel)}.`, + `Function key: ${functionName}.`, + `Value: ${JSON.stringify(functionValue)}`, + ].join('\n')); + } +} + +function getIpcChannelIdentifier(namespace: string, key: string) { + return `proxy:${namespace}:${key}`; +} + +type AsyncMethods = { + [P in keyof T]: T[P] extends (...args: infer Args) => infer R + ? R extends Promise + ? (...args: Args) => R + : (...args: Args) => Promise + : never; +}; diff --git a/src/presentation/electron/shared/IpcBridging/README.md b/src/presentation/electron/shared/IpcBridging/README.md new file mode 100644 index 00000000..05bd7749 --- /dev/null +++ b/src/presentation/electron/shared/IpcBridging/README.md @@ -0,0 +1,17 @@ +# IPC bridging + +This module introduces structured and type-safe inter-process communication (IPC) to Electron applications, +enhancing the development and maintenance of complex features. + +## Benefits + +- **Type safety**: Ensures reliable data exchange between processes and prevents runtime errors through enforced + type checks in IPC communication. +- **Maintainability**: Facilitates easy tracking and management of inter-process contracts using defined and clear + interfaces. +- **Security**: Implements the least-privilege principle by defining which members are accessible in proxy objects, + enhancing the security of IPC interactions. +- **Simplicity**: Simplifies IPC calls by abstracting the underlying complexity, providing a more straightforward + interface for developers. +- **Scalability**: Structured IPC management supports effective scaling and reduces the challenges of ad-hoc + IPC implementations. diff --git a/src/presentation/index.html b/src/presentation/index.html new file mode 100644 index 00000000..ccef1d47 --- /dev/null +++ b/src/presentation/index.html @@ -0,0 +1,67 @@ + + + + + + + privacy.sexy - Maximize Your Privacy and Security + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/src/presentation/injectionSymbols.ts b/src/presentation/injectionSymbols.ts new file mode 100644 index 00000000..3ba79669 --- /dev/null +++ b/src/presentation/injectionSymbols.ts @@ -0,0 +1,94 @@ +import { inject, type InjectionKey } from 'vue'; +import type { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState'; +import type { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication'; +import type { useRuntimeEnvironment } from '@/presentation/components/Shared/Hooks/UseRuntimeEnvironment'; +import type { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/UseClipboard'; +import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode'; +import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents'; +import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState'; +import type { useLogger } from '@/presentation/components/Shared/Hooks/Log/UseLogger'; +import type { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner'; +import type { useDialog } from '@/presentation/components/Shared/Hooks/Dialog/UseDialog'; +import type { useScriptDiagnosticsCollector } from '@/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector'; +import type { useAutoUnsubscribedEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener'; + +export const InjectionKeys = { + useCollectionState: defineTransientKey>('useCollectionState'), + useApplication: defineSingletonKey>('useApplication'), + useRuntimeEnvironment: defineSingletonKey>('useRuntimeEnvironment'), + useAutoUnsubscribedEvents: defineTransientKey>('useAutoUnsubscribedEvents'), + useClipboard: defineTransientKey>('useClipboard'), + useCurrentCode: defineTransientKey>('useCurrentCode'), + useUserSelectionState: defineTransientKey>('useUserSelectionState'), + useLogger: defineTransientKey>('useLogger'), + useCodeRunner: defineTransientKey>('useCodeRunner'), + useDialog: defineTransientKey>('useDialog'), + useScriptDiagnosticsCollector: defineTransientKey>('useScriptDiagnostics'), + useAutoUnsubscribedEventListener: defineTransientKey>('useAutoUnsubscribedEventListener'), +}; + +export interface InjectionKeyWithLifetime { + readonly lifetime: InjectionKeyLifetime; + readonly key: InjectionKey & symbol; +} + +export interface SingletonKey extends InjectionKeyWithLifetime { + readonly lifetime: InjectionKeyLifetime.Singleton; + readonly key: InjectionKey & symbol; +} + +export interface TransientKey extends InjectionKeyWithLifetime<() => T> { + readonly lifetime: InjectionKeyLifetime.Transient; + readonly key: InjectionKey<() => T> & symbol; +} + +export type AnyLifetimeInjectionKey = InjectionKeyWithLifetime | TransientKey; + +export type InjectionKeySelector = (keys: typeof InjectionKeys) => AnyLifetimeInjectionKey; + +export function injectKey( + keySelector: InjectionKeySelector, + vueInjector = inject, +): T { + const key = keySelector(InjectionKeys); + const injectedValue = injectRequired(key.key, vueInjector); + if (key.lifetime === InjectionKeyLifetime.Transient) { + const factory = injectedValue as () => T; + const value = factory(); + return value; + } + + return injectedValue as T; +} + +export enum InjectionKeyLifetime { + Singleton, + Transient, +} + +function defineSingletonKey(key: string): SingletonKey { + return { + lifetime: InjectionKeyLifetime.Singleton, + key: Symbol(key), + }; +} + +function defineTransientKey(key: string): TransientKey { + return { + lifetime: InjectionKeyLifetime.Transient, + key: Symbol(key), + }; +} + +function injectRequired( + key: InjectionKey, + vueInjector = inject, +): T { + const injectedValue = vueInjector(key); + + if (injectedValue === undefined) { + throw new Error(`Failed to inject value for key: ${key.description}`); + } + + return injectedValue; +} diff --git a/src/presentation/main.ts b/src/presentation/main.ts new file mode 100644 index 00000000..a35578a8 --- /dev/null +++ b/src/presentation/main.ts @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import App from './components/App.vue'; +import { ApplicationBootstrapper } from './bootstrapping/ApplicationBootstrapper'; + +const app = createApp(App); + +await new ApplicationBootstrapper() + .bootstrap(app); + +app.mount('#app'); diff --git a/src/presentation/public/favicon.ico b/src/presentation/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d0389d8fc219c6216bc1da1c6fa601af4305a463 GIT binary patch literal 372526 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 KFM&O^8B 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 literal 0 HcmV?d00001 diff --git a/src/presentation/public/icon.png b/src/presentation/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a8a6babb104f2a87b7e157b43b636373c91f9fc2 GIT binary patch literal 24057 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 D1F()m literal 0 HcmV?d00001 diff --git a/src/presentation/public/robots.txt b/src/presentation/public/robots.txt new file mode 100644 index 00000000..6f27bb66 --- /dev/null +++ b/src/presentation/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: \ No newline at end of file diff --git a/tests/.eslintrc.cjs b/tests/.eslintrc.cjs new file mode 100644 index 00000000..0e6acda5 --- /dev/null +++ b/tests/.eslintrc.cjs @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'no-console': 'off', + }, +}; diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..e7b8369e --- /dev/null +++ b/tests/README.md @@ -0,0 +1,3 @@ +# tests + +See [`tests.md`](./../docs/tests.md) diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/README.md b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/README.md new file mode 100644 index 00000000..9fc9837d --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/README.md @@ -0,0 +1,29 @@ +# check-desktop-runtime-errors + +This script automates the processes of: + +1) Building +2) Packaging +3) Installing +4) Executing +5) Verifying Electron distributions + +It runs the application for a duration and detects runtime errors in the packaged application via: + +- **Log verification**: Checking application logs for errors and validating successful application initialization. +- **`stderr` monitoring**: Continuous listening to the `stderr` stream for unexpected errors. +- **Window title inspection**: Checking for window titles that indicate crashes before logging becomes possible. + +Upon error, the script captures a screenshot (if `--screenshot` is provided) and terminates. + +## Options + +- `--build`: Clears the electron distribution directory and forces a rebuild of the Electron app. +- `--screenshot`: Takes a screenshot of the desktop environment after running the application. + +This module provides utilities for building, executing, and validating Electron desktop apps. +It can be used to automate checking for runtime errors during development. + +## Configs + +Configurations are defined in [`config.ts`](./config.ts). diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/app-logs.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/app-logs.ts new file mode 100644 index 00000000..d225ba50 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/app-logs.ts @@ -0,0 +1,91 @@ +import { unlink, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { log, die, LogLevel } from '../utils/log'; +import { exists } from '../utils/io'; +import { SupportedPlatform, CURRENT_PLATFORM } from '../utils/platform'; +import { getAppName } from '../utils/npm'; + +export async function clearAppLogFiles( + projectDir: string, +): Promise { + if (!projectDir) { throw new Error('missing project directory'); } + const logPath = await determineLogPath(projectDir); + if (!logPath || !await exists(logPath)) { + log(`Skipping clearing logs, log file does not exist: ${logPath}.`); + return; + } + try { + await unlink(logPath); + log(`Successfully cleared the log file at: ${logPath}.`); + } catch (error) { + die(`Failed to clear the log file at: ${logPath}. Reason: ${error}`); + } +} + +export async function readAppLogFile( + projectDir: string, +): Promise { + if (!projectDir) { throw new Error('missing project directory'); } + const logPath = await determineLogPath(projectDir); + if (!logPath || !await exists(logPath)) { + log(`No log file at: ${logPath}`, LogLevel.Warn); + return { + logFilePath: logPath, + }; + } + const logContent = await readLogFile(logPath); + return { + logFileContent: logContent, + logFilePath: logPath, + }; +} + +interface AppLogFileResult { + readonly logFilePath: string; + readonly logFileContent?: string; +} + +async function determineLogPath( + projectDir: string, +): Promise { + if (!projectDir) { throw new Error('missing project directory'); } + const logFileName = 'main.log'; + const appName = await getAppName(projectDir); + if (!appName) { + return die('App name not found.'); + } + const logFilePaths: { + readonly [K in SupportedPlatform]: () => string; + } = { + [SupportedPlatform.macOS]: () => { + if (!process.env.HOME) { + throw new Error('HOME environment variable is not defined'); + } + return join(process.env.HOME, 'Library', 'Logs', appName, logFileName); + }, + [SupportedPlatform.Linux]: () => { + if (!process.env.HOME) { + throw new Error('HOME environment variable is not defined'); + } + return join(process.env.HOME, '.config', appName, 'logs', logFileName); + }, + [SupportedPlatform.Windows]: () => { + if (!process.env.USERPROFILE) { + throw new Error('USERPROFILE environment variable is not defined'); + } + return join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', logFileName); + }, + }; + const logFilePath = logFilePaths[CURRENT_PLATFORM]?.(); + if (!logFilePath) { + log(`Cannot determine log path, unsupported OS: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn); + } + return logFilePath; +} + +async function readLogFile( + logFilePath: string, +): Promise { + const content = await readFile(logFilePath, 'utf-8'); + return content?.trim().length > 0 ? content : undefined; +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/check-for-errors.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/check-for-errors.ts new file mode 100644 index 00000000..a5821bc8 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/check-for-errors.ts @@ -0,0 +1,177 @@ +import { indentText } from '@/application/Common/Text/IndentText'; +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; +import { log, die } from '../utils/log'; +import { readAppLogFile } from './app-logs'; +import { STDERR_IGNORE_PATTERNS } from './error-ignore-patterns'; + +const ELECTRON_CRASH_TITLE = 'Error'; // Used by electron for early crashes +const LOG_ERROR_MARKER = '[error]'; // from electron-log +const EXPECTED_LOG_MARKERS = [ + '[WINDOW_INIT]', + '[PRELOAD_INIT]', + '[APP_INIT]', +]; + +export async function checkForErrors( + stderr: string, + windowTitles: readonly string[], + projectDir: string, +) { + if (!projectDir) { throw new Error('missing project directory'); } + const errors = await gatherErrors(stderr, windowTitles, projectDir); + if (errors.length) { + die(formatErrors(errors)); + } +} + +async function gatherErrors( + stderr: string, + windowTitles: readonly string[], + projectDir: string, +): Promise { + if (!projectDir) { throw new Error('missing project directory'); } + const { logFileContent: mainLogs, logFilePath: mainLogFile } = await readAppLogFile(projectDir); + const allLogs = [mainLogs, stderr].filter(Boolean).join('\n'); + return [ + verifyStdErr(stderr), + verifyApplicationLogsExist(mainLogs, mainLogFile), + ...EXPECTED_LOG_MARKERS.map( + (marker) => verifyLogMarkerExistsInLogs(allLogs, marker), + ), + verifyWindowTitle(windowTitles), + verifyErrorsInLogs(allLogs), + ].filter((error): error is ExecutionError => Boolean(error)); +} + +interface ExecutionError { + readonly reason: string; + readonly description: string; +} + +function formatErrors(errors: readonly ExecutionError[]): string { + if (!errors?.length) { throw new Error('missing errors'); } + return [ + 'Errors detected during execution:', + ...errors.map( + (error) => formatError(error), + ), + ].join('\n---\n'); +} + +function formatError(error: ExecutionError): string { + if (!error) { throw new Error('missing error'); } + if (!error.reason) { throw new Error(`missing reason, error (${typeof error}): ${JSON.stringify(error)}`); } + let message = `Reason: ${indentText(error.reason, 1)}`; + if (error.description) { + message += `\nDescription:\n${indentText(error.description, 2)}`; + } + return message; +} + +function verifyApplicationLogsExist( + logContent: string | undefined, + logFilePath: string, +): ExecutionError | undefined { + if (!logContent?.length) { + return describeError( + 'Missing application logs', + 'Application logs are empty not were not found.' + + `\nLog path: ${logFilePath}`, + ); + } + return undefined; +} + +function verifyLogMarkerExistsInLogs( + logContent: string | undefined, + marker: string, +) : ExecutionError | undefined { + if (!marker) { + throw new Error('missing marker'); + } + if (!logContent?.includes(marker)) { + return describeError( + 'Incomplete application logs', + `Missing identifier "${marker}" in application logs.`, + ); + } + return undefined; +} + +function verifyWindowTitle( + windowTitles: readonly string[], +) : ExecutionError | undefined { + const errorTitles = windowTitles.filter( + (title) => title.toLowerCase().includes(ELECTRON_CRASH_TITLE), + ); + if (errorTitles.length) { + return describeError( + 'Unexpected window title', + 'One or more window titles suggest an error occurred in the application:' + + `\nError Titles: ${errorTitles.join(', ')}` + + `\nAll Titles: ${windowTitles.join(', ')}`, + ); + } + return undefined; +} + +function verifyStdErr( + stderrOutput: string | undefined, +) : ExecutionError | undefined { + if (stderrOutput && stderrOutput.length > 0) { + const ignoredErrorLines = new Set(); + const relevantErrors = getNonEmptyLines(stderrOutput) + .filter((line) => { + line = line.trim(); + if (STDERR_IGNORE_PATTERNS.some((pattern) => pattern.test(line))) { + ignoredErrorLines.add(line); + return false; + } + return true; + }); + if (ignoredErrorLines.size > 0) { + log(`Ignoring \`stderr\` lines:\n${indentText([...ignoredErrorLines].join('\n'), 1)}`); + } + if (relevantErrors.length === 0) { + return undefined; + } + return describeError( + 'Standard error stream (`stderr`) is not empty.', + `Relevant errors (${relevantErrors.length}):\n${indentText(relevantErrors.map((error) => `- ${error}`).join('\n'), 1)}` + + `\nFull \`stderr\` output:\n${indentText(stderrOutput, 1)}`, + ); + } + return undefined; +} + +function verifyErrorsInLogs( + logContent: string | undefined, +) : ExecutionError | undefined { + if (!logContent?.length) { + return undefined; + } + const logLines = getNonEmptyLines(logContent) + .filter((line) => line.includes(LOG_ERROR_MARKER)); + if (!logLines.length) { + return undefined; + } + return describeError( + 'Application log file', + logLines.join('\n'), + ); +} + +function describeError( + reason: string, + description: string, +) : ExecutionError | undefined { + return { + reason, + description: `${description}\n\nThis might indicate an early crash or significant runtime issue.`, + }; +} + +function getNonEmptyLines(text: string) { + return splitTextIntoLines(text) + .filter((line) => line.trim().length > 0); +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/error-ignore-patterns.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/error-ignore-patterns.ts new file mode 100644 index 00000000..c90a0ad3 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/error-ignore-patterns.ts @@ -0,0 +1,41 @@ +/* eslint-disable vue/max-len */ + +/* Ignore errors specific to host environment, rather than application execution */ +export const STDERR_IGNORE_PATTERNS: readonly RegExp[] = [ + /* + OS: Linux + Background: + GLIBC and libgiolibproxy.so were seen on local Linux (Ubuntu-based) installation. + Original logs: + /snap/core20/current/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.29' not found (required by /lib/x86_64-linux-gnu/libproxy.so.1) + Failed to load module: /home/bob/snap/code/common/.cache/gio-modules/libgiolibproxy.so + [334053:0829/122143.595703:ERROR:browser_main_loop.cc(274)] GLib: Failed to set scheduler settings: Operation not permitted + */ + /libstdc\+\+\.so.*?GLIBCXX_.*?not found/, + /Failed to load module: .*?libgiolibproxy\.so/, + /\[.*?:ERROR:browser_main_loop\.cc.*?\] GLib: Failed to set scheduler settings: Operation not permitted/, + + /* + OS: macOS + Background: + Observed when running on GitHub runner, but not on local macOS environment. + Original logs: + [1571:0828/162611.460587:ERROR:trust_store_mac.cc(844)] Error parsing certificate: + ERROR: Failed parsing extensions + */ + /ERROR:trust_store_mac\.cc.*?Error parsing certificate:/, + /ERROR: Failed parsing extensions/, + + /* + OS: Linux (GitHub Actions) + Background: + Occur during Electron's GPU process initialization. Common in headless CI/CD environments. + Not indicative of a problem in typical desktop environments. + Original logs: + [3548:0828/162502.835833:ERROR:viz_main_impl.cc(186)] Exiting GPU process due to errors during initialization + [3627:0828/162503.133178:ERROR:viz_main_impl.cc(186)] Exiting GPU process due to errors during initialization + [3621:0828/162503.420173:ERROR:command_buffer_proxy_impl.cc(128)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. + */ + /ERROR:viz_main_impl\.cc.*?Exiting GPU process due to errors during initialization/, + /ERROR:command_buffer_proxy_impl\.cc.*?ContextResult::kTransientFailure: Failed to send GpuControl\.CreateCommandBuffer\./, +]; diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/common/app-artifact-locator.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/common/app-artifact-locator.ts new file mode 100644 index 00000000..698b2aa0 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/common/app-artifact-locator.ts @@ -0,0 +1,46 @@ +import { join } from 'node:path'; +import { readdir } from 'node:fs/promises'; +import { die } from '../../../utils/log'; +import { exists } from '../../../utils/io'; +import { getAppName } from '../../../utils/npm'; + +export async function findByFilePattern( + pattern: string, + directory: string, + projectRootDir: string, +): Promise { + if (!directory) { throw new Error('Missing directory'); } + if (!pattern) { throw new Error('Missing file pattern'); } + + if (!await exists(directory)) { + return die(`Directory does not exist: ${directory}`); + } + + const directoryContents = await readdir(directory); + const appName = await getAppName(projectRootDir); + const regexPattern = pattern + /* eslint-disable no-template-curly-in-string */ + .replaceAll('${name}', escapeRegExp(appName)) + .replaceAll('${version}', '\\d+\\.\\d+\\.\\d+') + .replaceAll('${ext}', '.*'); + /* eslint-enable no-template-curly-in-string */ + const regex = new RegExp(`^${regexPattern}$`); + const foundFileNames = directoryContents.filter((file) => regex.test(file)); + if (!foundFileNames.length) { + return die(`No files found matching pattern "${pattern}" in ${directory} directory.`); + } + if (foundFileNames.length > 1) { + return die(`Found multiple files matching pattern "${pattern}": ${foundFileNames.join(', ')}`); + } + return { + absolutePath: join(directory, foundFileNames[0]), + }; +} + +function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +interface ArtifactLocation { + readonly absolutePath: string; +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/common/extraction-result.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/common/extraction-result.ts new file mode 100644 index 00000000..531b0db1 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/common/extraction-result.ts @@ -0,0 +1,4 @@ +export interface ExtractionResult { + readonly appExecutablePath: string; + readonly cleanup?: () => Promise; +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/linux.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/linux.ts new file mode 100644 index 00000000..b9024bf5 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/linux.ts @@ -0,0 +1,40 @@ +import { access, chmod } from 'node:fs/promises'; +import { constants } from 'node:fs'; +import { log } from '../../utils/log'; +import { findByFilePattern } from './common/app-artifact-locator'; +import type { ExtractionResult } from './common/extraction-result'; + +export async function prepareLinuxApp( + desktopDistPath: string, + projectRootDir: string, +): Promise { + const { absolutePath: appFile } = await findByFilePattern( + // eslint-disable-next-line no-template-curly-in-string + '${name}-${version}.AppImage', + desktopDistPath, + projectRootDir, + ); + await makeExecutable(appFile); + return { + appExecutablePath: appFile, + }; +} + +async function makeExecutable(appFile: string): Promise { + if (!appFile) { throw new Error('missing file'); } + if (await isExecutable(appFile)) { + log('AppImage is already executable.'); + return; + } + log('Making it executable...'); + await chmod(appFile, 0o755); +} + +async function isExecutable(file: string): Promise { + try { + await access(file, constants.X_OK); + return true; + } catch { + return false; + } +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/macos.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/macos.ts new file mode 100644 index 00000000..ff2ead6b --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/macos.ts @@ -0,0 +1,86 @@ +import { runCommand } from '../../utils/run-command'; +import { exists } from '../../utils/io'; +import { log, die, LogLevel } from '../../utils/log'; +import { sleep } from '../../utils/sleep'; +import { findByFilePattern } from './common/app-artifact-locator'; +import type { ExtractionResult } from './common/extraction-result'; + +export async function prepareMacOsApp( + desktopDistPath: string, + projectRootDir: string, +): Promise { + const { absolutePath: dmgPath } = await findByFilePattern( + // eslint-disable-next-line no-template-curly-in-string + '${name}-${version}.dmg', + desktopDistPath, + projectRootDir, + ); + const { mountPath } = await mountDmg(dmgPath); + const appPath = await findMacAppExecutablePath(mountPath); + return { + appExecutablePath: appPath, + cleanup: async () => { + log('Cleaning up resources...'); + await detachMount(mountPath); + }, + }; +} + +async function mountDmg( + dmgFile: string, +) { + const { stdout: hdiutilOutput, error } = await runCommand( + `hdiutil attach '${dmgFile}'`, + ); + if (error) { + die(`Failed to mount DMG file at ${dmgFile}.\n${error}`); + } + const mountPathMatch = hdiutilOutput.match(/\/Volumes\/[^\n]+/); + if (!mountPathMatch || mountPathMatch.length === 0) { + die(`Could not find mount path from \`hdiutil\` output:\n${hdiutilOutput}`); + } + const mountPath = mountPathMatch[0]; + return { + mountPath, + }; +} + +async function findMacAppExecutablePath( + mountPath: string, +): Promise { + const { stdout: findOutput, error } = await runCommand( + `find '${mountPath}' -maxdepth 1 -type d -name "*.app"`, + ); + if (error) { + return die(`Failed to find executable path at mount path ${mountPath}\n${error}`); + } + const appFolder = findOutput.trim(); + const appName = appFolder.split('/').pop()?.replace('.app', ''); + if (!appName) { + die(`Could not extract app path from \`find\` output: ${findOutput}`); + } + const appPath = `${appFolder}/Contents/MacOS/${appName}`; + if (await exists(appPath)) { + log(`Application is located at ${appPath}`); + } else { + return die(`Application does not exist at ${appPath}`); + } + return appPath; +} + +async function detachMount( + mountPath: string, + retries = 5, +) { + const { error } = await runCommand(`hdiutil detach '${mountPath}'`); + if (error) { + if (retries <= 0) { + log(`Failed to detach mount after multiple attempts: ${mountPath}\n${error}`, LogLevel.Warn); + return; + } + await sleep(500); + await detachMount(mountPath, retries - 1); + return; + } + log(`Successfully detached from ${mountPath}`); +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/windows.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/windows.ts new file mode 100644 index 00000000..0ae8bb7b --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/extractors/windows.ts @@ -0,0 +1,58 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { exists } from '../../utils/io'; +import { log, die, LogLevel } from '../../utils/log'; +import { runCommand } from '../../utils/run-command'; +import { findByFilePattern } from './common/app-artifact-locator'; +import type { ExtractionResult } from './common/extraction-result'; + +export async function prepareWindowsApp( + desktopDistPath: string, + projectRootDir: string, +): Promise { + const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-')); + if (await exists(workdir)) { + log(`Temporary directory ${workdir} already exists, cleaning up...`); + await rm(workdir, { recursive: true }); + } + const appExecutablePath = await installNsis(workdir, desktopDistPath, projectRootDir); + return { + appExecutablePath, + cleanup: async () => { + log(`Cleaning up working directory ${workdir}...`); + try { + await rm(workdir, { recursive: true, force: true }); + } catch (error) { + log(`Could not cleanup the working directory: ${error.message}`, LogLevel.Error); + } + }, + }; +} + +async function installNsis( + installationPath: string, + desktopDistPath: string, + projectRootDir: string, +): Promise { + const { absolutePath: installerPath } = await findByFilePattern( + // eslint-disable-next-line no-template-curly-in-string + '${name}-Setup-${version}.exe', + desktopDistPath, + projectRootDir, + ); + log(`Silently installing contents of ${installerPath} to ${installationPath}...`); + const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`); + if (error) { + return die(`Failed to install.\n${error}`); + } + + const { absolutePath: appExecutablePath } = await findByFilePattern( + // eslint-disable-next-line no-template-curly-in-string + '${name}.exe', + installationPath, + projectRootDir, + ); + + return appExecutablePath; +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/runner.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/runner.ts new file mode 100644 index 00000000..8c7b8b28 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/runner.ts @@ -0,0 +1,201 @@ +import { spawn, type ChildProcess } from 'node:child_process'; +import { log, LogLevel, die } from '../utils/log'; +import { captureScreen } from './system-capture/screen-capture'; +import { captureWindowTitles } from './system-capture/window-title-capture'; + +const TERMINATION_GRACE_PERIOD_IN_SECONDS = 20; +const TERMINATION_CHECK_INTERVAL_IN_MS = 1000; +const WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS = 100; + +export function runApplication( + appFile: string, + executionDurationInSeconds: number, + enableScreenshot: boolean, + screenshotPath: string, +): Promise { + if (!appFile) { + throw new Error('Missing app file'); + } + + logDetails(appFile, executionDurationInSeconds); + + const process = spawn(appFile); + const processDetails: ApplicationProcessDetails = { + stderrData: '', + stdoutData: '', + explicitlyKilled: false, + windowTitles: [], + isCrashed: false, + isDone: false, + process, + resolve: () => { /* NOOP */ }, + }; + + return new Promise((resolve) => { + processDetails.resolve = resolve; + if (process.pid === undefined) { + throw new Error('Unknown PID'); + } + beginCapturingTitles(process.pid, processDetails); + handleProcessEvents( + processDetails, + enableScreenshot, + screenshotPath, + executionDurationInSeconds, + ); + }); +} + +interface ApplicationExecutionResult { + readonly stderr: string, + readonly stdout: string, + readonly windowTitles: readonly string[], + readonly isCrashed: boolean, +} + +interface ApplicationProcessDetails { + readonly process: ChildProcess; + + stderrData: string; + stdoutData: string; + explicitlyKilled: boolean; + windowTitles: Array; + isCrashed: boolean; + isDone: boolean; + resolve: (value: ApplicationExecutionResult) => void; +} + +function logDetails( + appFile: string, + executionDurationInSeconds: number, +): void { + log( + [ + 'Executing the app to check for errors...', + `Maximum execution time: ${executionDurationInSeconds}`, + `Application path: ${appFile}`, + ].join('\n\t'), + ); +} + +function beginCapturingTitles( + processId: number, + processDetails: ApplicationProcessDetails, +): void { + const capture = async () => { + const titles = await captureWindowTitles(processId); + + (titles || []).forEach((title) => { + if (!title) { + return; + } + if (!processDetails.windowTitles.includes(title)) { + log(`New window title captured: ${title}`); + processDetails.windowTitles.push(title); + } + }); + + if (!processDetails.isDone) { + setTimeout(capture, WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS); + } + }; + + capture(); +} + +function handleProcessEvents( + processDetails: ApplicationProcessDetails, + enableScreenshot: boolean, + screenshotPath: string, + executionDurationInSeconds: number, +): void { + const { process } = processDetails; + process.stderr?.on('data', (data) => { + processDetails.stderrData += data.toString(); + }); + process.stdout?.on('data', (data) => { + processDetails.stdoutData += data.toString(); + }); + + process.on('error', (error) => { + die(`An issue spawning the child process: ${error}`); + }); + + process.on('exit', async (code) => { + await onProcessExit(code, processDetails, enableScreenshot, screenshotPath); + }); + + setTimeout(async () => { + await onExecutionLimitReached(processDetails, enableScreenshot, screenshotPath); + }, executionDurationInSeconds * 1000); +} + +async function onProcessExit( + code: number | null, + processDetails: ApplicationProcessDetails, + enableScreenshot: boolean, + screenshotPath: string, +): Promise { + log(`Application exited ${code === null || Number.isNaN(code) ? '.' : `with code ${code}`}`); + + if (processDetails.explicitlyKilled) return; + + processDetails.isCrashed = true; + + if (enableScreenshot) { + await captureScreen(screenshotPath); + } + + finishProcess(processDetails); +} + +async function onExecutionLimitReached( + processDetails: ApplicationProcessDetails, + enableScreenshot: boolean, + screenshotPath: string, +): Promise { + if (enableScreenshot) { + await captureScreen(screenshotPath); + } + + processDetails.explicitlyKilled = true; + await terminateGracefully(processDetails.process); + finishProcess(processDetails); +} + +function finishProcess(processDetails: ApplicationProcessDetails): void { + processDetails.isDone = true; + processDetails.resolve({ + stderr: processDetails.stderrData, + stdout: processDetails.stdoutData, + windowTitles: [...processDetails.windowTitles], + isCrashed: processDetails.isCrashed, + }); +} + +async function terminateGracefully( + process: ChildProcess, +): Promise { + let elapsedSeconds = 0; + log('Attempting to terminate the process gracefully...'); + process.kill('SIGTERM'); + + return new Promise((resolve) => { + const checkInterval = setInterval(() => { + elapsedSeconds += TERMINATION_CHECK_INTERVAL_IN_MS / 1000; + + if (elapsedSeconds >= TERMINATION_GRACE_PERIOD_IN_SECONDS) { + process.kill('SIGKILL'); + log('Process did not terminate gracefully within the grace period. Forcing termination.', LogLevel.Warn); + clearInterval(checkInterval); + resolve(); + } + }, TERMINATION_CHECK_INTERVAL_IN_MS); + + process.on('exit', () => { + log('Process terminated gracefully.'); + clearInterval(checkInterval); + resolve(); + }); + }); +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/system-capture/screen-capture.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/system-capture/screen-capture.ts new file mode 100644 index 00000000..b8655231 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/system-capture/screen-capture.ts @@ -0,0 +1,63 @@ +import { unlink } from 'node:fs/promises'; +import { runCommand } from '../../utils/run-command'; +import { log, LogLevel } from '../../utils/log'; +import { CURRENT_PLATFORM, SupportedPlatform } from '../../utils/platform'; +import { exists } from '../../utils/io'; + +export async function captureScreen( + imagePath: string, +): Promise { + if (!imagePath) { + throw new Error('Path for screenshot not provided'); + } + + if (await exists(imagePath)) { + log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LogLevel.Warn); + unlink(imagePath); + } + + const platformCommands: { + readonly [K in SupportedPlatform]: string; + } = { + [SupportedPlatform.macOS]: `screencapture -x ${imagePath}`, + [SupportedPlatform.Linux]: `import -window root ${imagePath}`, + [SupportedPlatform.Windows]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`, + }; + + const commandForPlatform = platformCommands[CURRENT_PLATFORM]; + + if (!commandForPlatform) { + log(`Screenshot capture not supported on: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn); + return; + } + + log(`Capturing screenshot to ${imagePath} using command:\n\t> ${commandForPlatform}`); + + const { error } = await runCommand(commandForPlatform); + if (error) { + log(`Failed to capture screenshot.\n${error}`, LogLevel.Warn); + return; + } + log(`Captured screenshot to ${imagePath}.`); +} + +function getScreenshotPowershellScript(imagePath: string): string { + return ` + $ProgressPreference = 'SilentlyContinue' # Do not pollute stderr + Add-Type -AssemblyName System.Windows.Forms + $screenBounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds + + $bmp = New-Object System.Drawing.Bitmap $screenBounds.Width, $screenBounds.Height + $graphics = [System.Drawing.Graphics]::FromImage($bmp) + $graphics.CopyFromScreen([System.Drawing.Point]::Empty, [System.Drawing.Point]::Empty, $screenBounds.Size) + + $bmp.Save('${imagePath}') + $graphics.Dispose() + $bmp.Dispose() + `; +} + +function encodeForPowershell(script: string): string { + const buffer = Buffer.from(script, 'utf16le'); + return buffer.toString('base64'); +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/system-capture/window-title-capture.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/system-capture/window-title-capture.ts new file mode 100644 index 00000000..f8539e16 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/app/system-capture/window-title-capture.ts @@ -0,0 +1,124 @@ +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; +import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings'; +import { runCommand } from '../../utils/run-command'; +import { log, LogLevel } from '../../utils/log'; +import { SupportedPlatform, CURRENT_PLATFORM } from '../../utils/platform'; + +export async function captureWindowTitles(processId: number) { + if (!processId) { throw new Error('Missing process ID.'); } + + const captureFunction = windowTitleCaptureFunctions[CURRENT_PLATFORM]; + if (!captureFunction) { + log(`Cannot capture window title, unsupported OS: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn); + return undefined; + } + + return captureFunction(processId); +} + +const windowTitleCaptureFunctions: { + readonly [K in SupportedPlatform]: (processId: number) => Promise; +} = { + [SupportedPlatform.macOS]: (processId) => captureTitlesOnMac(processId), + [SupportedPlatform.Linux]: (processId) => captureTitlesOnLinux(processId), + [SupportedPlatform.Windows]: (processId) => captureTitlesOnWindows(processId), +}; + +async function captureTitlesOnWindows(processId: number): Promise { + if (!processId) { throw new Error('Missing process ID.'); } + + const { stdout: tasklistOutput, error } = await runCommand( + `tasklist /FI "PID eq ${processId}" /fo list /v`, + ); + if (error) { + log(`Failed to retrieve window title.\n${error}`, LogLevel.Warn); + return []; + } + const regex = /Window Title:\s*(.*)/; + const match = regex.exec(tasklistOutput); + if (match && match.length > 1 && match[1]) { + const title = match[1].trim(); + if (title === 'N/A') { + return []; + } + return [title]; + } + return []; +} + +async function captureTitlesOnLinux(processId: number): Promise { + if (!processId) { throw new Error('Missing process ID.'); } + + const { stdout: windowIdsOutput, error: windowIdError } = await runCommand( + `xdotool search --pid '${processId}'`, + ); + + if (windowIdError || !windowIdsOutput) { + return []; + } + + const windowIds = splitTextIntoLines(windowIdsOutput.trim()); + + const titles = await Promise.all(windowIds.map(async (windowId) => { + const { stdout: titleOutput, error: titleError } = await runCommand( + `xprop -id ${windowId} | grep "WM_NAME(STRING)" | cut -d '=' -f 2 | sed 's/^[[:space:]]*"\\(.*\\)"[[:space:]]*$/\\1/'`, + ); + if (titleError || !titleOutput) { + return undefined; + } + return titleOutput.trim(); + })); + + return filterEmptyStrings(titles); +} + +let hasAssistiveAccessOnMac = true; + +async function captureTitlesOnMac(processId: number): Promise { + if (!processId) { throw new Error('Missing process ID.'); } + if (!hasAssistiveAccessOnMac) { + return []; + } + const command = constructAppleScriptCommand(` + tell application "System Events" + try + set targetProcess to first process whose unix id is ${processId} + on error + return + end try + tell targetProcess + set allWindowNames to {} + repeat with aWindow in windows + set end of allWindowNames to name of aWindow + end repeat + return allWindowNames + end tell + end tell + `); + const { stdout: titleOutput, error } = await runCommand(command); + if (error) { + let errorMessage = ''; + if (error.includes('-25211')) { + errorMessage += 'Capturing window title requires assistive access. You do not have it.\n'; + hasAssistiveAccessOnMac = false; + } + errorMessage += error; + log(errorMessage, LogLevel.Warn); + return []; + } + const title = titleOutput?.trim(); + if (!title) { + return []; + } + return [title]; +} + +function constructAppleScriptCommand(appleScriptCode: string): string { + const scriptLines = splitTextIntoLines(appleScriptCode.trim()); + const trimmedLines = scriptLines.map((line) => line.trim()); + const nonEmptyLines = filterEmptyStrings(trimmedLines); + const formattedArguments = nonEmptyLines + .map((line) => `-e '${line.trim()}'`) + .join(' '); + return `osascript ${formattedArguments}`; +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/cli-args.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/cli-args.ts new file mode 100644 index 00000000..dd85c150 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/cli-args.ts @@ -0,0 +1,36 @@ +import { log } from './utils/log'; + +export enum CommandLineFlag { + ForceRebuild, + TakeScreenshot, +} + +export const COMMAND_LINE_FLAGS: { + readonly [key in CommandLineFlag]: string; +} = Object.freeze({ + [CommandLineFlag.ForceRebuild]: '--build', + [CommandLineFlag.TakeScreenshot]: '--screenshot', +}); + +export function logCurrentArgs(): void { + const processArguments = getProcessArguments(); + if (!processArguments.length) { + log('No additional arguments provided.'); + return; + } + log(`Arguments: ${processArguments.join(', ')}`); +} + +export function hasCommandLineFlag(flag: CommandLineFlag): boolean { + return getProcessArguments() + .includes(COMMAND_LINE_FLAGS[flag]); +} + +/* + Fetches process arguments dynamically each time the function is called. + This design allows for runtime modifications to process.argv, supporting scenarios + where the command-line arguments might be altered dynamically. +*/ +function getProcessArguments(): string[] { + return process.argv.slice(2); +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/config.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/config.ts new file mode 100644 index 00000000..f68fc3c3 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/config.ts @@ -0,0 +1,13 @@ +import { join } from 'node:path'; +import distDirs from '@/../dist-dirs.json' assert { type: 'json' }; + +export const DESKTOP_BUILD_COMMAND = [ + 'npm run electron:prebuild', + 'npm run check:verify-build-artifacts -- --electron-unbundled', + 'npm run electron:build -- --publish never', + 'npm run check:verify-build-artifacts -- --electron-bundled', +].join(' && '); +export const PROJECT_DIR = process.cwd(); +export const DESKTOP_DIST_PATH = join(PROJECT_DIR, distDirs.electronBundled); +export const APP_EXECUTION_DURATION_IN_SECONDS = 60; // Long enough for CI runners +export const SCREENSHOT_PATH = join(PROJECT_DIR, 'screenshot.png'); diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/index.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/index.ts new file mode 100644 index 00000000..3d44da4c --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/index.ts @@ -0,0 +1,3 @@ +import { main } from './main'; + +await main(); diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/main.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/main.ts new file mode 100644 index 00000000..47546fea --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/main.ts @@ -0,0 +1,72 @@ +import { indentText } from '@/application/Common/Text/IndentText'; +import { logCurrentArgs, CommandLineFlag, hasCommandLineFlag } from './cli-args'; +import { log, die } from './utils/log'; +import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm'; +import { clearAppLogFiles } from './app/app-logs'; +import { checkForErrors } from './app/check-for-errors'; +import { runApplication } from './app/runner.js'; +import { CURRENT_PLATFORM, SupportedPlatform } from './utils/platform'; +import { prepareLinuxApp } from './app/extractors/linux'; +import { prepareWindowsApp } from './app/extractors/windows.js'; +import { prepareMacOsApp } from './app/extractors/macos'; +import { + DESKTOP_BUILD_COMMAND, + PROJECT_DIR, + DESKTOP_DIST_PATH, + APP_EXECUTION_DURATION_IN_SECONDS, + SCREENSHOT_PATH, +} from './config'; +import type { ExtractionResult } from './app/extractors/common/extraction-result'; + +export async function main(): Promise { + logCurrentArgs(); + await ensureNpmProjectDir(PROJECT_DIR); + await npmInstall(PROJECT_DIR); + await npmBuild( + PROJECT_DIR, + DESKTOP_BUILD_COMMAND, + DESKTOP_DIST_PATH, + hasCommandLineFlag(CommandLineFlag.ForceRebuild), + ); + await clearAppLogFiles(PROJECT_DIR); + const { + stderr, stdout, isCrashed, windowTitles, + } = await extractAndRun(); + if (stdout) { + log(`Output (stdout) from application execution:\n${indentText(stdout, 1)}`); + } + if (isCrashed) { + die('The application encountered an error during its execution.'); + } + await checkForErrors(stderr, windowTitles, PROJECT_DIR); + log('🥳🎈 Success! Application completed without any runtime errors.'); + process.exit(0); +} + +async function extractAndRun() { + const extractors: { + readonly [K in SupportedPlatform]: () => Promise; + } = { + [SupportedPlatform.macOS]: () => prepareMacOsApp(DESKTOP_DIST_PATH, PROJECT_DIR), + [SupportedPlatform.Linux]: () => prepareLinuxApp(DESKTOP_DIST_PATH, PROJECT_DIR), + [SupportedPlatform.Windows]: () => prepareWindowsApp(DESKTOP_DIST_PATH, PROJECT_DIR), + }; + const extractor = extractors[CURRENT_PLATFORM]; + if (!extractor) { + throw new Error(`Platform not supported: ${SupportedPlatform[CURRENT_PLATFORM]}`); + } + const { appExecutablePath, cleanup } = await extractor(); + try { + return await runApplication( + appExecutablePath, + APP_EXECUTION_DURATION_IN_SECONDS, + hasCommandLineFlag(CommandLineFlag.TakeScreenshot), + SCREENSHOT_PATH, + ); + } finally { + if (cleanup) { + log('Cleaning up post-execution resources...'); + await cleanup(); + } + } +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/io.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/io.ts new file mode 100644 index 00000000..9599301a --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/io.ts @@ -0,0 +1,21 @@ +import { readdir, access } from 'node:fs/promises'; +import { constants } from 'node:fs'; + +export async function exists(path: string): Promise { + if (!path) { throw new Error('Missing path'); } + try { + await access(path, constants.F_OK); + return true; + } catch { + return false; + } +} + +export async function isDirMissingOrEmpty(dir: string): Promise { + if (!dir) { throw new Error('Missing directory'); } + if (!await exists(dir)) { + return true; + } + const contents = await readdir(dir); + return contents.length === 0; +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/log.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/log.ts new file mode 100644 index 00000000..9edc911a --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/log.ts @@ -0,0 +1,68 @@ +export enum LogLevel { + Info, + Warn, + Error, +} + +export function log(message: string, level = LogLevel.Info): void { + const timestamp = new Date().toISOString(); + const config = LOG_LEVEL_CONFIG[level] || LOG_LEVEL_CONFIG[LogLevel.Info]; + const logLevelText = `${getColorCode(config.color)}${LOG_LEVEL_LABELS[level]}${getColorCode(TextColor.Reset)}`; + const formattedMessage = `[${timestamp}][${logLevelText}] ${message}`; + config.method(formattedMessage); +} + +export function die(message: string): never { + log(message, LogLevel.Error); + return process.exit(1); +} + +enum TextColor { + Reset, + LightRed, + Yellow, + LightBlue, +} + +function getColorCode(color: TextColor): string { + return COLOR_CODE_MAPPING[color]; +} + +const LOG_LEVEL_LABELS: { + readonly [K in LogLevel]: string; +} = { + [LogLevel.Info]: 'INFO', + [LogLevel.Error]: 'ERROR', + [LogLevel.Warn]: 'WARN', +}; + +const COLOR_CODE_MAPPING: { + readonly [K in TextColor]: string; +} = { + [TextColor.Reset]: '\x1b[0m', + [TextColor.LightRed]: '\x1b[91m', + [TextColor.Yellow]: '\x1b[33m', + [TextColor.LightBlue]: '\x1b[94m', +}; + +interface ColorLevelConfig { + readonly color: TextColor; + readonly method: (...data: unknown[]) => void; +} + +const LOG_LEVEL_CONFIG: { + readonly [K in LogLevel]: ColorLevelConfig; +} = { + [LogLevel.Info]: { + color: TextColor.LightBlue, + method: console.log, + }, + [LogLevel.Warn]: { + color: TextColor.Yellow, + method: console.warn, + }, + [LogLevel.Error]: { + color: TextColor.LightRed, + method: console.error, + }, +}; diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/npm.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/npm.ts new file mode 100644 index 00000000..0fe27357 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/npm.ts @@ -0,0 +1,104 @@ +import { join } from 'node:path'; +import { rm, readFile } from 'node:fs/promises'; +import { exists, isDirMissingOrEmpty } from './io'; +import { runCommand } from './run-command'; +import { LogLevel, die, log } from './log'; + +export async function ensureNpmProjectDir(projectDir: string): Promise { + if (!projectDir) { throw new Error('missing project directory'); } + if (!await exists(join(projectDir, 'package.json'))) { + die(`\`package.json\` not found in project directory: ${projectDir}`); + } +} + +export async function npmInstall(projectDir: string): Promise { + if (!projectDir) { throw new Error('missing project directory'); } + const npmModulesPath = join(projectDir, 'node_modules'); + if (!await isDirMissingOrEmpty(npmModulesPath)) { + log(`Directory "${npmModulesPath}" exists and has content. Skipping installing dependencies.`); + return; + } + log('Starting dependency installation...'); + const { error } = await runCommand( + `npm run install-deps -- --no-errors --root-directory ${projectDir}`, + { + cwd: projectDir, + }, + ); + if (error) { + die(error); + } + log('Installed dependencies...'); +} + +export async function npmBuild( + projectDir: string, + buildCommand: string, + distDir: string, + forceRebuild: boolean, +): Promise { + if (!projectDir) { throw new Error('missing project directory'); } + if (!buildCommand) { throw new Error('missing build command'); } + if (!distDir) { throw new Error('missing distribution directory'); } + + const isMissingBuild = await isDirMissingOrEmpty(distDir); + + if (!isMissingBuild && !forceRebuild) { + log(`Directory "${distDir}" exists and has content. Skipping build: '${buildCommand}'.`); + return; + } + + if (forceRebuild) { + log(`Removing directory "${distDir}" for a clean build (triggered by \`--build\` flag).`); + await rm(distDir, { recursive: true, force: true }); + } + + log('Building project...'); + const { error } = await runCommand(buildCommand, { + cwd: projectDir, + }); + if (error) { + die(error); + } + + if (await isDirMissingOrEmpty(distDir)) { + die(`The desktop application build process did not produce the expected artifacts. The output directory "${distDir}" is empty or missing.`); + } +} + +const appNameCache = new Map(); + +export async function getAppName(projectDir: string): Promise { + if (!projectDir) { throw new Error('missing project directory'); } + const cachedName = appNameCache.get(projectDir); + if (cachedName) { + return cachedName; + } + const packageData = await readPackageJsonContents(projectDir); + try { + const packageJson = JSON.parse(packageData); + const name = packageJson.name as string | undefined; + if (!name) { + return die(`The \`package.json\` file doesn't specify a name: ${packageData}`); + } + appNameCache.set(projectDir, name); + return name; + } catch (error) { + return die(`Unable to parse \`package.json\`. Error: ${error}\nContent: ${packageData}`); + } +} + +async function readPackageJsonContents(projectDir: string): Promise { + if (!projectDir) { throw new Error('missing project directory'); } + const packagePath = join(projectDir, 'package.json'); + if (!await exists(packagePath)) { + return die(`\`package.json\` file not found at ${packagePath}`); + } + try { + const packageData = await readFile(packagePath, 'utf8'); + return packageData; + } catch (error) { + log(`Error reading \`package.json\` from ${packagePath}.`, LogLevel.Error); + return die(`Error detail: ${error}`); + } +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/platform.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/platform.ts new file mode 100644 index 00000000..d638204a --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/platform.ts @@ -0,0 +1,31 @@ +import { platform } from 'node:os'; +import { die } from './log'; + +export enum SupportedPlatform { + macOS, + Windows, + Linux, +} + +const NODE_PLATFORM_MAPPINGS: { + readonly [K in SupportedPlatform]: NodeJS.Platform; +} = { + [SupportedPlatform.macOS]: 'darwin', + [SupportedPlatform.Linux]: 'linux', + [SupportedPlatform.Windows]: 'win32', +}; + +function getCurrentPlatform(): SupportedPlatform | never { + const nodePlatform = platform(); + + for (const key of Object.keys(NODE_PLATFORM_MAPPINGS)) { + const keyAsSupportedPlatform = parseInt(key, 10) as SupportedPlatform; + if (NODE_PLATFORM_MAPPINGS[keyAsSupportedPlatform] === nodePlatform) { + return keyAsSupportedPlatform; + } + } + + return die(`Unsupported platform: ${nodePlatform}`); +} + +export const CURRENT_PLATFORM: SupportedPlatform = getCurrentPlatform(); diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/run-command.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/run-command.ts new file mode 100644 index 00000000..20040bf6 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/run-command.ts @@ -0,0 +1,58 @@ +import { exec } from 'child_process'; +import { indentText } from '@/application/Common/Text/IndentText'; +import type { ExecOptions, ExecException } from 'child_process'; + +const TIMEOUT_IN_SECONDS = 180; +const MAX_OUTPUT_BUFFER_SIZE = 1024 * 1024; // 1 MB + +export function runCommand( + command: string, + options?: ExecOptions, +): Promise { + return new Promise((resolve) => { + options = { + cwd: process.cwd(), + timeout: TIMEOUT_IN_SECONDS * 1000, + maxBuffer: MAX_OUTPUT_BUFFER_SIZE * 2, + ...(options ?? {}), + }; + + exec(command, options, (error, stdout, stderr) => { + let errorText: string | undefined; + if (error || stderr?.length > 0) { + errorText = formatError(command, error, stdout, stderr); + } + resolve({ + stdout, + error: errorText, + }); + }); + }); +} + +export interface CommandResult { + readonly stdout: string; + readonly error?: string; +} + +function formatError( + command: string, + error: ExecException | null, + stdout: string | undefined, + stderr: string | undefined, +) { + const errorParts = [ + 'Error while running command.', + `Command:\n${indentText(command, 1)}`, + ]; + if (error?.toString().trim()) { + errorParts.push(`Error:\n${indentText(error.toString(), 1)}`); + } + if (stderr?.trim()) { + errorParts.push(`stderr:\n${indentText(stderr, 1)}`); + } + if (stdout?.trim()) { + errorParts.push(`stdout:\n${indentText(stdout, 1)}`); + } + return errorParts.join('\n---\n'); +} diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/sleep.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/sleep.ts new file mode 100644 index 00000000..0b80a7b2 --- /dev/null +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/sleep.ts @@ -0,0 +1,5 @@ +export function sleep(milliseconds: number) { + return new Promise((resolve) => { + setTimeout(resolve, milliseconds); + }); +} diff --git a/tests/checks/desktop-runtime-errors/main.spec.ts b/tests/checks/desktop-runtime-errors/main.spec.ts new file mode 100644 index 00000000..7e49fadf --- /dev/null +++ b/tests/checks/desktop-runtime-errors/main.spec.ts @@ -0,0 +1,88 @@ +import { + describe, it, beforeAll, afterAll, +} from 'vitest'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { main } from './check-desktop-runtime-errors/main'; +import { COMMAND_LINE_FLAGS, CommandLineFlag } from './check-desktop-runtime-errors/cli-args'; + +describe('desktop runtime error checks', () => { + const { waitForExitCode } = useInterceptedProcessExitOrCompletion(beforeAll, afterAll); + it('should successfully execute the main function and exit with a zero status code', async () => { + // arrange + setCommandLineFlagsFromEnvironmentVariables(); + // act + const exitCode = await waitForExitCode( + () => main(), + ); + // assert + expect(exitCode).to.equal(0, formatAssertionMessage([ + `Test failed with exit code ${exitCode}; expected 0.`, + 'Examine preceding logs to identify errors.', + ])); + }, { + timeout: 60 /* minutes */ * 60000, + }); +}); + +function useInterceptedProcessExitOrCompletion( + beforeTest: (callback: () => void) => void, + afterTest: (callback: () => void) => void, +) { + const originalFunction = global.process.exit; + let isExitCodeReceived = false; + let exitCodeResolver: (value: number | undefined) => void; + const waitForExitCode = (runner: () => Promise) => new Promise( + (resolve, reject) => { + exitCodeResolver = resolve; + runner() + .catch((error) => { + if (isExitCodeReceived) { + return; + } + console.error('Process did not call `process.exit` but threw an error:', error); + reject(error); + }) + .then(() => { + if (isExitCodeReceived) { + return; + } + console.log('Process completed without calling `process.exit`. Treating as `0` exit code.'); + exitCodeResolver(0); + }); + }, + ); + beforeTest(() => { + global.process.exit = (code?: number): never => { + exitCodeResolver(code); + isExitCodeReceived = true; + return undefined as never; + }; + }); + afterTest(() => { + global.process.exit = originalFunction; + }); + return { + waitForExitCode, + }; +} + +/* + Map environment variables to CLI arguments for compatibility with Vitest. +*/ +function setCommandLineFlagsFromEnvironmentVariables() { + const flagEnvironmentVariableKeyMappings: { + readonly [key in CommandLineFlag]: string; + } = { + [CommandLineFlag.ForceRebuild]: 'BUILD', + [CommandLineFlag.TakeScreenshot]: 'SCREENSHOT', + }; + Object.entries(flagEnvironmentVariableKeyMappings) + .forEach(([flag, environmentVariableKey]) => { + if (process.env[environmentVariableKey] !== undefined) { + process.argv = [ + ...process.argv, + COMMAND_LINE_FLAGS[flag], + ]; + } + }); +} diff --git a/tests/checks/external-urls/DocumentationUrlExtractor.ts b/tests/checks/external-urls/DocumentationUrlExtractor.ts new file mode 100644 index 00000000..7b53b0d1 --- /dev/null +++ b/tests/checks/external-urls/DocumentationUrlExtractor.ts @@ -0,0 +1,69 @@ +import type { IApplication } from '@/domain/IApplication'; +import type { TestExecutionDetailsLogger } from './TestExecutionDetailsLogger'; + +interface UrlExtractionContext { + readonly logger: TestExecutionDetailsLogger; + readonly application: IApplication; + readonly urlExclusionPatterns: readonly RegExp[]; +} + +export function extractDocumentationUrls( + context: UrlExtractionContext, +): string[] { + const urlsInApplication = extractUrlsFromApplication(context.application); + context.logger.logLabeledInformation( + 'Extracted URLs from application', + urlsInApplication.length.toString(), + ); + const uniqueUrls = filterDuplicateUrls(urlsInApplication); + context.logger.logLabeledInformation( + 'Unique URLs after deduplication', + `${uniqueUrls.length} (duplicates removed)`, + ); + context.logger.logLabeledInformation( + 'Exclusion patterns for URLs', + context.urlExclusionPatterns.length === 0 + ? 'None (all URLs included)' + : context.urlExclusionPatterns.map((pattern, index) => `${index + 1}) ${pattern.toString()}`).join('\n'), + ); + const includedUrls = filterUrlsExcludingPatterns(uniqueUrls, context.urlExclusionPatterns); + context.logger.logLabeledInformation( + 'URLs extracted for testing', + `${includedUrls.length} (after applying exclusion patterns; ${uniqueUrls.length - includedUrls.length} URLs ignored)`, + ); + return includedUrls; +} + +function extractUrlsFromApplication(application: IApplication): string[] { + return [ // Get all executables + ...application.collections.flatMap((c) => c.getAllCategories()), + ...application.collections.flatMap((c) => c.getAllScripts()), + ] + // Get all docs + .flatMap((documentable) => documentable.docs) + // Parse all URLs + .flatMap((docString) => extractUrlsExcludingCodeBlocks(docString)); +} + +function filterDuplicateUrls(urls: readonly string[]): string[] { + return urls.filter((url, index, array) => array.indexOf(url) === index); +} + +function filterUrlsExcludingPatterns( + urls: readonly string[], + patterns: readonly RegExp[], +): string[] { + return urls.filter((url) => !patterns.some((pattern) => pattern.test(url))); +} + +function extractUrlsExcludingCodeBlocks(textWithInlineCode: string): string[] { + /* + Matches URLs: + - Excludes inline code blocks as they may contain URLs not intended for user interaction + and not guaranteed to support expected HTTP methods, leading to false-negatives. + - Supports URLs containing parentheses, avoiding matches within code that might not represent + actual links. + */ + const nonCodeBlockUrlRegex = /(?()]+(?:\([^\s`"<>()]*\))?[^\s`"<>()]*)/g; + return textWithInlineCode.match(nonCodeBlockUrlRegex) || []; +} diff --git a/tests/checks/external-urls/StatusChecker/BatchStatusChecker.ts b/tests/checks/external-urls/StatusChecker/BatchStatusChecker.ts new file mode 100644 index 00000000..7f9eae70 --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/BatchStatusChecker.ts @@ -0,0 +1,78 @@ +import { sleep } from '@/infrastructure/Threading/AsyncSleep'; +import { getUrlStatus, type RequestOptions } from './Requestor'; +import { groupUrlsByDomain } from './UrlDomainProcessing'; +import type { FollowOptions } from './FetchFollow'; +import type { UrlStatus } from './UrlStatus'; + +export async function getUrlStatusesInParallel( + urls: string[], + options?: BatchRequestOptions, +): Promise { + // urls = ['https://privacy.sexy']; // Comment out this line to use a hardcoded URL for testing. + const uniqueUrls = Array.from(new Set(urls)); + const defaultedDomainOptions: Required = { + ...DefaultDomainOptions, + ...options?.domainOptions, + }; + console.log('Batch request options applied:', defaultedDomainOptions); + const results = await request(uniqueUrls, defaultedDomainOptions, options); + return results; +} + +export interface BatchRequestOptions { + readonly domainOptions?: Partial; + readonly requestOptions?: Partial; + readonly followOptions?: Partial; +} + +interface DomainOptions { + readonly sameDomainParallelize?: boolean; + readonly sameDomainDelayInMs?: number; +} + +const DefaultDomainOptions: Required = { + sameDomainParallelize: false, + sameDomainDelayInMs: 3 /* sec */ * 1000, +}; + +function request( + urls: string[], + domainOptions: Required, + options?: BatchRequestOptions, +): Promise { + if (!domainOptions.sameDomainParallelize) { + return runOnEachDomainWithDelay( + urls, + (url) => getUrlStatus(url, options?.requestOptions, options?.followOptions), + domainOptions.sameDomainDelayInMs, + ); + } + return Promise.all( + urls.map((url) => getUrlStatus(url, options?.requestOptions, options?.followOptions)), + ); +} + +async function runOnEachDomainWithDelay( + urls: string[], + action: (url: string) => Promise, + delayInMs: number | undefined, +): Promise { + const grouped = groupUrlsByDomain(urls); + const tasks = grouped.map(async (group) => { + const results = new Array(); + /* eslint-disable no-await-in-loop */ + for (const url of group) { + const status = await action(url); + results.push(status); + if (results.length !== group.length) { + if (delayInMs !== undefined) { + await sleep(delayInMs); + } + } + } + /* eslint-enable no-await-in-loop */ + return results; + }); + const r = await Promise.all(tasks); + return r.flat(); +} diff --git a/tests/checks/external-urls/StatusChecker/ExponentialBackOffRetryHandler.ts b/tests/checks/external-urls/StatusChecker/ExponentialBackOffRetryHandler.ts new file mode 100644 index 00000000..0b5c60d1 --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/ExponentialBackOffRetryHandler.ts @@ -0,0 +1,56 @@ +import { sleep } from '@/infrastructure/Threading/AsyncSleep'; +import { indentText } from '@/application/Common/Text/IndentText'; +import { type UrlStatus, formatUrlStatus } from './UrlStatus'; + +const DefaultBaseRetryIntervalInMs = 5 /* sec */ * 1000; + +export async function retryWithExponentialBackOff( + action: () => Promise, + baseRetryIntervalInMs: number = DefaultBaseRetryIntervalInMs, + currentRetry = 1, +): Promise { + const maxTries = 3; + const status = await action(); + if (shouldRetry(status)) { + if (currentRetry <= maxTries) { + const exponentialBackOffInMs = getRetryTimeoutInMs(currentRetry, baseRetryIntervalInMs); + console.log([ + `Attempt ${currentRetry}: Retrying in ${exponentialBackOffInMs / 1000} seconds.`, + 'Details:', + indentText(formatUrlStatus(status)), + ].join('\n')); + await sleep(exponentialBackOffInMs); + return retryWithExponentialBackOff(action, baseRetryIntervalInMs, currentRetry + 1); + } + console.warn('💀 All retry attempts failed. Final failure to retrieve URL:', indentText(formatUrlStatus(status))); + } + return status; +} + +function shouldRetry(status: UrlStatus): boolean { + if (status.error) { + return true; + } + if (status.code === undefined) { + return true; + } + return isTransientError(status.code) + || status.code === 429; // Too Many Requests +} + +function isTransientError(statusCode: number): boolean { + return statusCode >= 500 && statusCode <= 599; +} + +function getRetryTimeoutInMs( + currentRetry: number, + baseRetryIntervalInMs: number = DefaultBaseRetryIntervalInMs, +): number { + const retryRandomFactor = 0.5; // Retry intervals are between 50% and 150% + // of the exponentially increasing base amount + const minRandom = 1 - retryRandomFactor; + const maxRandom = 1 + retryRandomFactor; + const randomization = (Math.random() * (maxRandom - minRandom)) + maxRandom; + const exponential = 2 ** (currentRetry - 1); + return Math.ceil(exponential * baseRetryIntervalInMs * randomization); +} diff --git a/tests/checks/external-urls/StatusChecker/FetchFollow.ts b/tests/checks/external-urls/StatusChecker/FetchFollow.ts new file mode 100644 index 00000000..2d6a1743 --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/FetchFollow.ts @@ -0,0 +1,122 @@ +import { indentText } from '@/application/Common/Text/IndentText'; +import { fetchWithTimeout } from './FetchWithTimeout'; +import { getDomainFromUrl } from './UrlDomainProcessing'; + +export function fetchFollow( + url: string, + timeoutInMs: number, + fetchOptions?: Partial, + followOptions?: Partial, +): Promise { + const defaultedFollowOptions: Required = { + ...DefaultFollowOptions, + ...followOptions, + }; + console.log(indentText(`Follow options: ${JSON.stringify(defaultedFollowOptions)}`)); + if (!followRedirects(defaultedFollowOptions)) { + return fetchWithTimeout(url, timeoutInMs, fetchOptions); + } + fetchOptions = { ...fetchOptions, redirect: 'manual' /* handled manually */, mode: 'cors' }; + const cookies = new CookieStorage(defaultedFollowOptions.enableCookies); + return followRecursivelyWithCookies( + url, + timeoutInMs, + fetchOptions, + defaultedFollowOptions.maximumRedirectFollowDepth, + cookies, + ); +} + +export interface FollowOptions { + readonly followRedirects?: boolean; + readonly maximumRedirectFollowDepth?: number; + readonly enableCookies?: boolean; +} + +const DefaultFollowOptions: Required = { + followRedirects: true, + maximumRedirectFollowDepth: 20, + enableCookies: true, +}; + +async function followRecursivelyWithCookies( + url: string, + timeoutInMs: number, + options: RequestInit, + followDepth: number, + cookies: CookieStorage, +): Promise { + options = updateCookieHeader(cookies, options); + const response = await fetchWithTimeout( + url, + timeoutInMs, + options, + ); + if (!isRedirect(response.status)) { + return response; + } + const newFollowDepth = followDepth - 1; + if (newFollowDepth < 0) { + throw new Error(`[max-redirect] maximum redirect reached at: ${url}`); + } + const nextUrl = response.headers.get('location'); + if (!nextUrl) { + return response; + } + const cookieHeader = response.headers.get('set-cookie'); + if (cookieHeader) { + cookies.addHeader(cookieHeader); + } + options.headers = { + ...options.headers, + Host: getDomainFromUrl(nextUrl), + }; + return followRecursivelyWithCookies(nextUrl, timeoutInMs, options, newFollowDepth, cookies); +} + +function isRedirect(code: number): boolean { + return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; +} + +class CookieStorage { + public cookies = new Array(); + + constructor(private readonly enabled: boolean) { + } + + public hasAny(): boolean { + return this.enabled && this.cookies.length > 0; + } + + public addHeader(header: string) { + if (!this.enabled || !header) { + return; + } + this.cookies.push(header); + } + + public getHeader(): string { + return this.cookies.join(' ; '); + } +} + +function followRedirects(options: FollowOptions): boolean { + if (options.followRedirects !== true) { + return false; + } + if (options.maximumRedirectFollowDepth === undefined || options.maximumRedirectFollowDepth <= 0) { + throw new Error('Invalid followRedirects configuration: maximumRedirectFollowDepth must be a positive integer'); + } + return true; +} + +function updateCookieHeader( + cookies: CookieStorage, + options: RequestInit, +): RequestInit { + if (!cookies.hasAny()) { + return options; + } + const newOptions = { ...options, headers: { ...options.headers, cookie: cookies.getHeader() } }; + return newOptions; +} diff --git a/tests/checks/external-urls/StatusChecker/FetchWithTimeout.ts b/tests/checks/external-urls/StatusChecker/FetchWithTimeout.ts new file mode 100644 index 00000000..135ad5ed --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/FetchWithTimeout.ts @@ -0,0 +1,14 @@ +export async function fetchWithTimeout( + url: string, + timeoutInMs: number, + init?: RequestInit, +): ReturnType { + const options: RequestInit = { + ...(init ?? {}), + signal: AbortSignal.timeout(timeoutInMs), + }; + return fetch( + url, + options, + ); +} diff --git a/tests/checks/external-urls/StatusChecker/README.md b/tests/checks/external-urls/StatusChecker/README.md new file mode 100644 index 00000000..078edb21 --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/README.md @@ -0,0 +1,109 @@ +# status-checker + +A CLI and SDK for checking the availability of external URLs. + +🧐 Why? + +- 🏃 **Fast**: Batch checks the statuses of URLs in parallel. +- 🤖 **Easy-to-Use**: Zero-touch startup with pre-configured settings for reliable results, yet customizable. +- 🤞 **Reliable**: Mimics real web browser behavior by following redirects and maintaining cookie storage. + +🍭 Additional features + +- 😇 **Rate Limiting**: Queues requests by domain to be polite. +- 🔁 **Retries**: Implements retry pattern with exponential back-off. +- ⌚ **Timeouts**: Configurable timeout for each request. +- 🎭️ **Impersonation**: Impersonate different browsers for each request. + - **🌐 User-Agent Rotation**: Change user agents. + - **🔑 TLS Handshakes**: Perform TLS and HTTP handshakes that are identical to that of a real browser. +- 🫙 **Cookie jar**: Preserve cookies during redirects to mimic real browser. + +## CLI + +Coming soon 🚧 + +## Programmatic usage + +The SDK supports both Node.js and browser environments. + +### `getUrlStatusesInParallel` + +```js +// Simple example +const statuses = await getUrlStatusesInParallel([ 'https://privacy.sexy', /* ... */ ]); +if(statuses.all((r) => r.code === 200)) { + console.log('All URLs are alive!'); +} else { + console.log('Dead URLs:', statuses.filter((r) => r.code !== 200).map((r) => r.url)); +} + +// Fastest configuration +const statuses = await getUrlStatusesInParallel([ 'https://privacy.sexy', /* ... */ ], { + domainOptions: { + sameDomainParallelize: false, + } +}); +``` + +#### Batch request options + +- `domainOptions`: + - **`sameDomainParallelize`**, (*boolean*), default: `false` + - Determines if requests to the same domain will be parallelized. + - Setting to `false` makes all requests parallel. + - Setting to `true` queues requests for each unique domain while parallelizing across different domains. + - Requests to different domains are always parallelized regardless of this option. + - 💡 This helps to avoid `429 Too Many Requests` and be nice to websites + - **`sameDomainDelayInMs`** (*number*), default: `3000` (3 seconds) + - Sets the delay between requests to the same domain. +- `requestOptions` (*object*): See [request options](#request-options). +- `followOptions` (*object*): See [follow options](#follow-options). + +### `getUrlStatus` + +Check the availability of a single URL. + +```js +// Simple example +const status = await getUrlStatus('https://privacy.sexy'); +console.log(`Status code: ${status.code}`); +``` + +#### Request options + +- **`retryExponentialBaseInMs`** (*number*), default: `5000` (5 seconds) + - Base time for the exponential back-off calculation for retries. + - The longer the base time, the greater the intervals between retries. +- **`additionalHeaders`** (*object*), default: `false` + - Additional HTTP headers to send along with the default headers. Overrides default headers if specified. +- **`requestTimeoutInMs`** (*number*), default: `60000` (60 seconds) + - Time limit to abort the request if no response is received within the specified time frame. + +### `fetchFollow` + +Follows `3XX` redirects while preserving cookies. + +Same fetch API except third parameter that specifies [follow options](#follow-options), `redirect: 'follow' | 'manual' | 'error'` is discarded in favor of the third parameter. + +```js +const status = await fetchFollow('https://privacy.sexy', 1000 /* timeout in milliseconds */); +console.log(`Status code: ${status.code}`); +``` + +#### Follow options + +- **`followRedirects`** (*boolean*), default: `true` + - Determines whether or not to follow redirects with `3XX` response codes. +- **`maximumRedirectFollowDepth`** (*boolean*), default: `20` + - Specifies the maximum number of sequential redirects that the function will follow. + - 💡 Helps to solve maximum redirect reached errors. +- **`enableCookies`** (*boolean*), default: `true` + - Enables cookie storage to facilitate seamless navigation through login or other authentication challenges. + - 💡 Helps to over-come sign-in challenges with callbacks. +- **`forceHttpGetForUrlPatterns`** (*array*), default: `[]` + - Specifies URL patterns that should always use an HTTP GET request instead of the default HTTP HEAD. + - This is useful for websites that do not respond to HEAD requests, such as those behind certain CDN or web application firewalls. + - Provide patterns as regular expressions (`RegExp`), allowing them to match any part of a URL. + - Examples: + - To match any URL starting with `https://example.com/api`: `/^https:\/\/example\.com\/api/` + - To match any domain ending with `cloudflare.com`: `/^https:\/\/.*\.cloudflare\.com\//` diff --git a/tests/checks/external-urls/StatusChecker/Requestor.ts b/tests/checks/external-urls/StatusChecker/Requestor.ts new file mode 100644 index 00000000..d5a88b41 --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/Requestor.ts @@ -0,0 +1,123 @@ +import { indentText } from '@/application/Common/Text/IndentText'; +import { retryWithExponentialBackOff } from './ExponentialBackOffRetryHandler'; +import { fetchFollow, type FollowOptions } from './FetchFollow'; +import { getRandomUserAgent } from './UserAgents'; +import { getDomainFromUrl } from './UrlDomainProcessing'; +import { randomizeTlsFingerprint, getTlsContextInfo } from './TlsFingerprintRandomizer'; +import type { UrlStatus } from './UrlStatus'; + +export function getUrlStatus( + url: string, + requestOptions?: Partial, + followOptions?: Partial, +): Promise { + const defaultedOptions = getDefaultedRequestOptions(requestOptions); + if (defaultedOptions.randomizeTlsFingerprint) { + randomizeTlsFingerprint(); + } + return fetchUrlStatusWithRetry(url, defaultedOptions, followOptions); +} + +export interface RequestOptions { + readonly retryExponentialBaseInMs?: number; + readonly additionalHeaders?: Record; + readonly additionalHeadersUrlIgnore?: string[]; + readonly requestTimeoutInMs: number; + readonly randomizeTlsFingerprint: boolean; + readonly forceHttpGetForUrlPatterns: RegExp[]; +} + +const DefaultOptions: Required = { + retryExponentialBaseInMs: 5 /* sec */ * 1000, + additionalHeaders: {}, + additionalHeadersUrlIgnore: [], + requestTimeoutInMs: 60 /* seconds */ * 1000, + randomizeTlsFingerprint: true, + forceHttpGetForUrlPatterns: [], +}; + +function fetchUrlStatusWithRetry( + url: string, + requestOptions: Required, + followOptions?: Partial, +): Promise { + const fetchOptions = getFetchOptions(url, requestOptions); + return retryWithExponentialBackOff(async () => { + console.log(`🚀 Initiating request for URL: ${url}`); + console.log(indentText([ + `HTTP method: ${fetchOptions.method}`, + `Request options: ${JSON.stringify(requestOptions)}`, + ].join('\n'))); + let result: UrlStatus; + try { + const response = await fetchFollow( + url, + requestOptions.requestTimeoutInMs, + fetchOptions, + followOptions, + ); + result = { url, code: response.status }; + } catch (err) { + result = { + url, + error: [ + 'Error:', indentText(JSON.stringify(err, null, '\t') || err.toString()), + 'Fetch options:', indentText(JSON.stringify(fetchOptions, null, '\t')), + 'Request options:', indentText(JSON.stringify(requestOptions, null, '\t')), + 'TLS:', indentText(getTlsContextInfo()), + ].join('\n'), + }; + } + return result; + }, requestOptions.retryExponentialBaseInMs); +} + +function getFetchOptions(url: string, options: Required): RequestInit { + const additionalHeaders = options.additionalHeadersUrlIgnore + .some((ignorePattern) => url.startsWith(ignorePattern)) + ? {} + : options.additionalHeaders; + return { + method: getHttpMethod(url, options), + headers: { + ...getDefaultHeaders(url), + ...additionalHeaders, + }, + redirect: 'manual', // Redirects are handled manually, automatic redirects do not work with Host header + }; +} + +function getHttpMethod(url: string, options: Required): 'HEAD' | 'GET' { + if (options.forceHttpGetForUrlPatterns.some((pattern) => url.match(pattern))) { + return 'GET'; + } + // By default fetch only headers without the full response body for better speed + return 'HEAD'; +} + +function getDefaultHeaders(url: string): Record { + return { + // Needed for websites that filter out non-browser user agents. + 'User-Agent': getRandomUserAgent(), + + // Required for some websites, especially those behind proxies, to correctly handle the request. + Host: getDomainFromUrl(url), + + // The following mimic a real browser request to improve compatibility with most web servers. + 'Upgrade-Insecure-Requests': '1', + Connection: 'keep-alive', + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate, br', + 'Cache-Control': 'max-age=0', + 'Accept-Language': 'en-US,en;q=0.9', + }; +} + +function getDefaultedRequestOptions( + options?: Partial, +): Required { + return { + ...DefaultOptions, + ...options, + }; +} diff --git a/tests/checks/external-urls/StatusChecker/TlsFingerprintRandomizer.ts b/tests/checks/external-urls/StatusChecker/TlsFingerprintRandomizer.ts new file mode 100644 index 00000000..11e1d0c6 --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/TlsFingerprintRandomizer.ts @@ -0,0 +1,69 @@ +/** + * Modifies the TLS fingerprint of Node.js HTTP client to circumvent TLS fingerprinting blocks. + * TLS fingerprinting is a technique used to identify clients based on the unencrypted data sent + * during the TLS handshake, used for blocking or identifying non-browser clients like debugging + * proxies or automated scripts. + * + * However, Node.js's HTTP client does not fully support all methods required for impersonating a + * browser's TLS fingerprint, as reported in https://github.com/nodejs/undici/issues/1983. + * While this implementation can alter the TLS fingerprint by randomizing the cipher suite order, + * it may not perfectly mimic specific browser fingerprints due to limitations in the TLS + * implementation of Node.js. + * + * For more detailed information, visit: + * - https://archive.today/2024.03.13-102042/https://httptoolkit.com/blog/tls-fingerprinting-node-js/ + * - https://check.ja3.zone/ (To check your tool's or browser's fingerprint) + * - https://github.com/lwthiker/curl-impersonate (A solution for curl) + * - https://github.com/depicts/got-tls (Cipher manipulation support for Node.js) + */ + +import { constants } from 'crypto'; +import tls from 'tls'; +import { indentText } from '@/application/Common/Text/IndentText'; + +export function randomizeTlsFingerprint() { + tls.DEFAULT_CIPHERS = getShuffledCiphers().join(':'); + console.log(indentText( + `TLS context:\n${indentText([ + 'Original ciphers:', indentText(constants.defaultCipherList), + 'Current ciphers:', indentText(getTlsContextInfo()), + ].join('\n'))}`, + )); +} + +export function getTlsContextInfo(): string { + return [ + `Ciphers: ${tls.DEFAULT_CIPHERS}`, + `Minimum TLS protocol version: ${tls.DEFAULT_MIN_VERSION}`, + `Node fingerprint: ${constants.defaultCoreCipherList === tls.DEFAULT_CIPHERS ? 'Visible' : 'Masked'}`, + ].join('\n'); +} + +/** + * Shuffles the order of TLS ciphers, excluding the top 3 most important ciphers to maintain + * security preferences. This approach modifies the default cipher list of Node.js to create a + * unique TLS fingerprint, thus helping to circumvent detection mechanisms based on static + * fingerprinting. It leverages randomness in the cipher order as a simple method to generate a + * new, unique TLS fingerprint which is not easily identifiable. The technique is based on altering + * parameters used in the TLS handshake process, particularly the cipher suite order, to avoid + * matching known fingerprints that could identify the client as a Node.js application. + * + * For more details, refer to: + * - https://archive.today/2024.03.13-102234/https://getsetfetch.org/blog/tls-fingerprint.html + */ +export function getShuffledCiphers(): readonly string[] { + const nodeOrderedCipherList = constants.defaultCoreCipherList.split(':'); + const totalTopCiphersToKeep = 3; + // Keep the most important ciphers in the same order + const fixedCiphers = nodeOrderedCipherList.slice(0, totalTopCiphersToKeep); + // Shuffle the rest + const shuffledCiphers = nodeOrderedCipherList.slice(totalTopCiphersToKeep) + .map((cipher) => ({ cipher, sort: Math.random() })) + .sort((a, b) => a.sort - b.sort) + .map(({ cipher }) => cipher); + const ciphers = [ + ...fixedCiphers, + ...shuffledCiphers, + ]; + return ciphers; +} diff --git a/tests/checks/external-urls/StatusChecker/UrlDomainProcessing.ts b/tests/checks/external-urls/StatusChecker/UrlDomainProcessing.ts new file mode 100644 index 00000000..4038fc10 --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/UrlDomainProcessing.ts @@ -0,0 +1,19 @@ +export function groupUrlsByDomain(urls: string[]): string[][] { + const domains = new Set(); + const urlsWithDomain = urls.map((url) => ({ + url, + domain: getDomainFromUrl(url), + })); + for (const url of urlsWithDomain) { + domains.add(url.domain); + } + return Array.from(domains).map((domain) => { + return urlsWithDomain + .filter((url) => url.domain.toLowerCase() === domain.toLowerCase()) + .map((url) => url.url); + }); +} + +export function getDomainFromUrl(url: string): string { + return new URL(url).host; +} diff --git a/tests/checks/external-urls/StatusChecker/UrlStatus.ts b/tests/checks/external-urls/StatusChecker/UrlStatus.ts new file mode 100644 index 00000000..c445fa34 --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/UrlStatus.ts @@ -0,0 +1,19 @@ +import { indentText } from '@/application/Common/Text/IndentText'; + +export interface UrlStatus { + readonly url: string; + readonly error?: string; + readonly code?: number; +} + +export function formatUrlStatus(status: UrlStatus): string { + return [ + `URL: ${status.url}`, + ...status.code !== undefined ? [ + `Response code: ${status.code}`, + ] : [], + ...status.error ? [ + `Error:\n${indentText(status.error)}`, + ] : [], + ].join('\n'); +} diff --git a/tests/checks/external-urls/StatusChecker/UserAgents.ts b/tests/checks/external-urls/StatusChecker/UserAgents.ts new file mode 100644 index 00000000..f8350ec4 --- /dev/null +++ b/tests/checks/external-urls/StatusChecker/UserAgents.ts @@ -0,0 +1,30 @@ +export function getRandomUserAgent(): string { + return UserAgents[Math.floor(Math.random() * UserAgents.length)]; +} + +const UserAgents = [ + // Safari 17.1 - macOS and iPad + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', + // Safari - iOS 17 - iPhone + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + // Safari - iOS 17 - iPad mini + 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + // Edge - macOS + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.51', + // Edge - Windows + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.58', + // Edge - Android + 'Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.43 Mobile Safari/537.36 EdgA/119.0.2151.92', + // Chrome - macOS + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + // Chrome - Windows + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + // Chrome - Android (Phone) + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36', + // Firefox - macOS + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0', + // Firefox - Windows + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0,', + // Firefox - Android (Phone) + 'Mozilla/5.0 (Android 14; Mobile; rv:109.0) Gecko/120.0 Firefox/120.0', +]; diff --git a/tests/checks/external-urls/TestExecutionDetailsLogger.ts b/tests/checks/external-urls/TestExecutionDetailsLogger.ts new file mode 100644 index 00000000..1dd4b02b --- /dev/null +++ b/tests/checks/external-urls/TestExecutionDetailsLogger.ts @@ -0,0 +1,26 @@ +import { indentText } from '@/application/Common/Text/IndentText'; + +export class TestExecutionDetailsLogger { + public logTestSectionStartDelimiter(): void { + this.logSectionDelimiterLine(); + } + + public logTestSectionEndDelimiter(): void { + this.logSectionDelimiterLine(); + } + + public logLabeledInformation( + label: string, + detailedInformation: string, + ): void { + console.log([ + `${label}:`, + indentText(detailedInformation), + ].join('\n')); + } + + private logSectionDelimiterLine(): void { + const horizontalLine = '─'.repeat(40); + console.log(horizontalLine); + } +} diff --git a/tests/checks/external-urls/main.spec.ts b/tests/checks/external-urls/main.spec.ts new file mode 100644 index 00000000..5cf98491 --- /dev/null +++ b/tests/checks/external-urls/main.spec.ts @@ -0,0 +1,100 @@ +import { test, expect } from 'vitest'; +import { parseApplication } from '@/application/Parser/ApplicationParser'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { shuffle } from '@/application/Common/Shuffle'; +import { indentText } from '@/application/Common/Text/IndentText'; +import { type UrlStatus, formatUrlStatus } from './StatusChecker/UrlStatus'; +import { getUrlStatusesInParallel, type BatchRequestOptions } from './StatusChecker/BatchStatusChecker'; +import { TestExecutionDetailsLogger } from './TestExecutionDetailsLogger'; +import { extractDocumentationUrls } from './DocumentationUrlExtractor'; + +// arrange +const logger = new TestExecutionDetailsLogger(); +logger.logTestSectionStartDelimiter(); +const app = parseApplication(); +let urls = extractDocumentationUrls({ + logger, + urlExclusionPatterns: [ + /^https:\/\/archive\.ph/, // Drops HEAD/GET requests via fetch/curl, responding to Postman/Chromium. + ], + application: app, +}); +urls = filterUrlsToEnvironmentCheckLimit(urls); +logger.logLabeledInformation('URLs submitted for testing', urls.length.toString()); +const requestOptions: BatchRequestOptions = { + domainOptions: { + sameDomainParallelize: false, // be nice to our third-party servers + sameDomainDelayInMs: 5 /* sec */ * 1000, + }, + requestOptions: { + retryExponentialBaseInMs: 3 /* sec */ * 1000, + requestTimeoutInMs: 60 /* sec */ * 1000, + additionalHeaders: { referer: app.projectDetails.homepage }, + randomizeTlsFingerprint: true, + }, + followOptions: { + followRedirects: true, + enableCookies: true, + }, +}; +logger.logLabeledInformation('HTTP request options', JSON.stringify(requestOptions, null, 2)); +const testTimeoutInMs = urls.length * 60 /* seconds */ * 1000; +logger.logLabeledInformation('Scheduled test duration', convertMillisecondsToHumanReadableFormat(testTimeoutInMs)); +logger.logTestSectionEndDelimiter(); +test(`all URLs (${urls.length}) should be alive`, async () => { + // act + const results = await getUrlStatusesInParallel(urls, requestOptions); + // assert + const deadUrls = results.filter((r) => r.code === undefined || !isOkStatusCode(r.code)); + expect(deadUrls).to.have.lengthOf( + 0, + formatAssertionMessage([createReportForDeadUrlStatuses(deadUrls)]), + ); +}, testTimeoutInMs); + +function isOkStatusCode(statusCode: number): boolean { + return statusCode >= 200 && statusCode < 300; +} + +function createReportForDeadUrlStatuses(deadUrlStatuses: readonly UrlStatus[]): string { + return `\n${deadUrlStatuses.map((status) => indentText(formatUrlStatus(status))).join('\n---\n')}\n`; +} + +function filterUrlsToEnvironmentCheckLimit(originalUrls: string[]): string[] { + const { RANDOMIZED_URL_CHECK_LIMIT } = process.env; + logger.logLabeledInformation('URL check limit', RANDOMIZED_URL_CHECK_LIMIT || 'Unlimited'); + if (RANDOMIZED_URL_CHECK_LIMIT !== undefined && RANDOMIZED_URL_CHECK_LIMIT !== '') { + const maxUrlsInTest = parseInt(RANDOMIZED_URL_CHECK_LIMIT, 10); + if (Number.isNaN(maxUrlsInTest)) { + throw new Error(`Invalid URL limit: ${RANDOMIZED_URL_CHECK_LIMIT}`); + } + if (maxUrlsInTest < originalUrls.length) { + return shuffle(originalUrls).slice(0, maxUrlsInTest); + } + } + return originalUrls; +} + +function convertMillisecondsToHumanReadableFormat(milliseconds: number): string { + const timeParts: string[] = []; + const addTimePart = (amount: number, label: string) => { + if (amount === 0) { + return; + } + timeParts.push(`${amount} ${label}`); + }; + + const hours = milliseconds / (1000 * 60 * 60); + const absoluteHours = Math.floor(hours); + addTimePart(absoluteHours, 'hours'); + + const minutes = (hours - absoluteHours) * 60; + const absoluteMinutes = Math.floor(minutes); + addTimePart(absoluteMinutes, 'minutes'); + + const seconds = (minutes - absoluteMinutes) * 60; + const absoluteSeconds = Math.floor(seconds); + addTimePart(absoluteSeconds, 'seconds'); + + return timeParts.join(', '); +} diff --git a/tests/e2e/.eslintrc.cjs b/tests/e2e/.eslintrc.cjs new file mode 100644 index 00000000..d6eefa35 --- /dev/null +++ b/tests/e2e/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + plugins: [ + 'cypress', + ], + env: { + 'cypress/globals': true, + }, + extends: [ + 'plugin:cypress/recommended', + ], + rules: { + strict: 'off', + }, +}; diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore new file mode 100644 index 00000000..2ca81ab1 --- /dev/null +++ b/tests/e2e/.gitignore @@ -0,0 +1,2 @@ +screenshots +videos \ No newline at end of file diff --git a/tests/e2e/card-list-layout-stability-on-load.cy.ts b/tests/e2e/card-list-layout-stability-on-load.cy.ts new file mode 100644 index 00000000..687a844e --- /dev/null +++ b/tests/e2e/card-list-layout-stability-on-load.cy.ts @@ -0,0 +1,226 @@ +// eslint-disable-next-line max-classes-per-file +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { getHeaderBrandTitle } from './support/interactions/header'; +import { ViewportTestScenarios } from './support/scenarios/viewport-test-scenarios'; + +interface Stoppable { + stop(): void; +} + +describe('card list layout stability', () => { + describe('during initial page load', () => { + const testCleanup = new Array(); + afterEach(() => { + testCleanup.forEach((c) => c.stop()); + testCleanup.length = 0; + }); + ViewportTestScenarios.forEach(({ name, width, height }) => { + it(`ensures layout stability on ${name}`, () => { + // arrange + const dimensions = new DimensionsStorage(); + cy.viewport(width, height); + // act + cy.window().then((win) => { + findElementFast(win, '.cards', (cardList) => { + testCleanup.push( + new SizeMonitor().start(cardList, () => dimensions.add(captureDimensions(cardList))), + ); + }); + testCleanup.push( + new ContinuousRunner() + .start(() => { + /* + As Cypress does not inherently support CPU throttling, this workaround is used to + intentionally slow down Cypress's execution. It allows capturing sudden layout + issues, such as brief flashes or shifts. + */ + cy.window().then(() => { + cy.log('Throttling'); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(50, { log: false }); + }); + }, 100), + ); + }); + cy.visit('/'); + for (const assertNextCheckpoint of Object.values(checkpoints)) { + assertNextCheckpoint(); + } + + // assert + const widthToleranceInPx = 0; + const widthsInPx = dimensions.getUniqueWidths(); + expect(isWithinTolerance(widthsInPx, widthToleranceInPx)) + .to.equal(true, formatAssertionMessage([ + `Unique width values over time: ${[...widthsInPx].join(', ')}`, + `Height changes are more than ${widthToleranceInPx}px tolerance`, + `Captured metrics: ${dimensions.toString()}`, + ])); + + const heightToleranceInPx = 100; // Set in relation to card sizes. + // Tolerance allows for minor layout shifts without (e.g. for icon or font loading) + // false test failures. The number `100` accounts for shifts when the number of + // cards per row changes, avoiding failures for shifts less than the smallest card + // size (~175px). + const heightsInPx = dimensions.getUniqueHeights(); + expect(isWithinTolerance(heightsInPx, heightToleranceInPx)) + .to.equal(true, formatAssertionMessage([ + `Unique height values over time: ${[...heightsInPx].join(', ')}`, + `Height changes are more than ${heightToleranceInPx}px tolerance`, + `Captured metrics: ${dimensions.toString()}`, + ])); + }); + }); + }); +}); + +/* + It finds a DOM element as quickly as possible. + It's crucial for detecting early layout shifts during page load, + which may be missed by standard Cypress commands such as `cy.get`, `cy.document`. +*/ +function findElementFast( + win: Cypress.AUTWindow, + query: string, + handler: (element: Element) => void, + timeoutInMs = 10000, +): void { + const endTime = Date.now() + timeoutInMs; + const finder = new ContinuousRunner(); + finder.start(() => { + const element = win.document.querySelector(query); + if (element) { + handler(element); + finder.stop(); + return; + } + if (Date.now() >= endTime) { + finder.stop(); + throw new Error(`Timed out. Failed to find element. Query: ${query}. Timeout: ${timeoutInMs}ms`); + } + }, 1 /* As aggressive as possible */); +} + +class DimensionsStorage { + private readonly dimensions = new Array(); + + public add(newDimension: SizeDimensions): void { + if (this.dimensions.length > 0) { + const lastDimension = this.dimensions[this.dimensions.length - 1]; + if (lastDimension.width === newDimension.width + && lastDimension.height === newDimension.height) { + return; + } + } + cy.window().then(() => { + cy.log(`Captured: ${JSON.stringify(newDimension)}`); + }); + this.dimensions.push(newDimension); + } + + public getUniqueWidths(): readonly number[] { + return [...new Set(this.dimensions.map((d) => d.width))]; + } + + public getUniqueHeights(): readonly number[] { + return [...new Set(this.dimensions.map((d) => d.height))]; + } + + public toString(): string { + return JSON.stringify(this.dimensions); + } +} + +function isWithinTolerance( + numbers: readonly number[], + tolerance: number, +) { + let changeWithinTolerance = true; + const [firstValue, ...otherValues] = numbers; + let previousValue = firstValue; + otherValues.forEach((value) => { + const difference = Math.abs(value - previousValue); + if (difference > tolerance) { + changeWithinTolerance = false; + } + previousValue = value; + }); + return changeWithinTolerance; +} + +interface SizeDimensions { + readonly width: number; + readonly height: number; +} + +function captureDimensions(element: Element): SizeDimensions { + const dimensions = element.getBoundingClientRect(); // more reliable than body.scroll... + return { + width: Math.round(dimensions.width), + height: Math.round(dimensions.height), + }; +} + +enum ApplicationLoadStep { + IndexHtmlLoaded = 0, + AppVueLoaded = 1, + HeaderBrandTitleLoaded = 2, +} + +const checkpoints: Record void> = { + [ApplicationLoadStep.IndexHtmlLoaded]: () => cy.get('#app').should('be.visible'), + [ApplicationLoadStep.AppVueLoaded]: () => cy.get('.app__wrapper').should('be.visible'), + [ApplicationLoadStep.HeaderBrandTitleLoaded]: () => getHeaderBrandTitle(), +}; + +class ContinuousRunner implements Stoppable { + private timer: ReturnType | undefined; + + public start(callback: () => void, intervalInMs: number): this { + this.stop(); + this.timer = setInterval(() => { + if (this.isStopped) { + return; + } + callback(); + }, intervalInMs); + return this; + } + + public stop() { + if (this.timer === undefined) { + return; + } + clearInterval(this.timer); + this.timer = undefined; + } + + private get isStopped() { + return this.timer === undefined; + } +} + +class SizeMonitor implements Stoppable { + private observer: ResizeObserver | undefined; + + public start(element: Element, sizeChangedCallback: () => void): this { + this.stop(); + this.observer = new ResizeObserver(() => { + if (this.isStopped) { + return; + } + sizeChangedCallback(); + }); + this.observer.observe(element); + return this; + } + + public stop() { + this.observer?.disconnect(); + this.observer = undefined; + } + + private get isStopped() { + return this.observer === undefined; + } +} diff --git a/tests/e2e/code-highlighting.cy.ts b/tests/e2e/code-highlighting.cy.ts new file mode 100644 index 00000000..f7de5217 --- /dev/null +++ b/tests/e2e/code-highlighting.cy.ts @@ -0,0 +1,40 @@ +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { getCurrentHighlightRange } from './support/interactions/code-area'; +import { selectAllScripts } from './support/interactions/script-selection'; +import { openCard } from './support/interactions/card'; + +describe('script selection highlighting', () => { + // Regression test for a bug where selecting multiple scripts only highlighted the last one. + it('highlights more when multiple scripts are selected', () => { + cy.visit('/'); + selectLastScript(); + getNonZeroCurrentHighlightRangeValue().then((lastScriptHighlightRange) => { + cy.log(`Highlight height for last script: ${lastScriptHighlightRange}`); + expect(lastScriptHighlightRange).to.be.greaterThan(0); + cy.visit('/'); + selectAllScripts(); + getNonZeroCurrentHighlightRangeValue().then((allScriptsHighlightRange) => { + cy.log(`Highlight height for all scripts: ${allScriptsHighlightRange}`); + expect(allScriptsHighlightRange).to.be.greaterThan(lastScriptHighlightRange); + }); + }); + }); +}); + +function getNonZeroCurrentHighlightRangeValue() { + return getCurrentHighlightRange() + .should('not.equal', '0') + .then((rangeValue) => { + expectExists(rangeValue); + return parseInt(rangeValue, 10); + }); +} + +function selectLastScript() { + openCard({ + cardIndex: -1, // last card + }); + cy.get('.node') + .last() + .click({ force: true }); +} diff --git a/tests/e2e/initialization.cy.ts b/tests/e2e/initialization.cy.ts new file mode 100644 index 00000000..89c8f206 --- /dev/null +++ b/tests/e2e/initialization.cy.ts @@ -0,0 +1,41 @@ +import { getHeaderBrandTitle } from './support/interactions/header'; + +describe('application is initialized as expected', () => { + it('loads title as expected', () => { + // act + cy.visit('/'); + // assert + getHeaderBrandTitle(); + }); + it('there are no console.error output', () => { + // act + // https://docs.cypress.io/faq/questions/using-cypress-faq#How-do-I-spy-on-console-log + cy.visit('/', { + onBeforeLoad(win) { + cy.stub(win.console, 'error').as('consoleError'); + }, + }); + // assert + cy.get('@consoleError').should('have.not.been.called'); + }); + it('there are no console.warn output', () => { + // act + cy.visit('/', { + onBeforeLoad(win) { + cy.stub(win.console, 'warn').as('consoleWarn'); + }, + }); + // assert + cy.get('@consoleWarn').should('have.not.been.called'); + }); + it('there are no console.log output', () => { + // act + cy.visit('/', { + onBeforeLoad(win) { + cy.stub(win.console, 'log').as('consoleLog'); + }, + }); + // assert + cy.get('@consoleLog').should('have.not.been.called'); + }); +}); diff --git a/tests/e2e/no-unintended-layout-shifts.cy.ts b/tests/e2e/no-unintended-layout-shifts.cy.ts new file mode 100644 index 00000000..aff76e04 --- /dev/null +++ b/tests/e2e/no-unintended-layout-shifts.cy.ts @@ -0,0 +1,79 @@ +import { ViewportTestScenarios, LargeScreen } from './support/scenarios/viewport-test-scenarios'; +import { openCard } from './support/interactions/card'; +import { selectAllScripts, unselectAllScripts } from './support/interactions/script-selection'; +import { assertLayoutStability } from './support/assert/layout-stability'; + +describe('Layout stability', () => { + ViewportTestScenarios.forEach(({ // some shifts are observed only on extra small or large screens + name, width, height, + }) => { + // Regression test for a bug where opening a modal caused layout shift + describe('Modal interaction', () => { + it(name, () => { + // arrange + cy.viewport(width, height); + cy.visit('/'); + // act & assert + assertLayoutStability('body', () => { + cy + .contains('button', 'Privacy') + .click(); + cy + .get('.modal-content-content') + .should('be.visible'); + }); + }); + }); + + // Regression test for a bug where selecting a script with an open card caused layout shift + describe('Initial script selection', () => { + it(name, () => { + // arrange + cy.viewport(width, height); + cy.visit('/'); + cy.contains('span', 'Windows') + .click(); + // act & assert + assertLayoutStability('#app', () => { + openCard({ + cardIndex: 0, + }); + selectAllScripts(); + }); + }); + }); + + // Regression test for a bug where unselecting selected with an open card caused layout shift + describe('Deselection script selection', () => { + it(name, () => { + // arrange + cy.viewport(width, height); + cy.visit('/'); + cy.contains('span', 'Windows') + .click(); + openCard({ + cardIndex: 0, + }); + selectAllScripts(); + // act & assert + assertLayoutStability('#app', () => { + unselectAllScripts(); + }); + }); + }); + }); + + // Regression test for bug on Chromium where horizontal scrollbar visibility causes layout shifts. + it('Scrollbar visibility', () => { + // arrange + cy.viewport(LargeScreen.width, LargeScreen.height); + cy.visit('/'); + openCard({ + cardIndex: 0, + }); + // act + assertLayoutStability('.app__wrapper', () => { + cy.viewport(LargeScreen.width, 100); // Set small height to trigger horizontal scrollbar. + }, { excludeHeight: true }); + }); +}); diff --git a/tests/e2e/no-unintended-overflow.cy.ts b/tests/e2e/no-unintended-overflow.cy.ts new file mode 100644 index 00000000..e6e3c429 --- /dev/null +++ b/tests/e2e/no-unintended-overflow.cy.ts @@ -0,0 +1,25 @@ +import { formatAssertionMessage } from '../shared/FormatAssertionMessage'; + +describe('has no unintended overflow', () => { + it('fits the content without horizontal scroll', () => { + // arrange + cy.viewport(375, 667); // iPhone SE + // act + cy.visit('/'); + // assert + cy.window().then((win) => { + expect(win.document.documentElement.scrollWidth, formatAssertionMessage([ + `Window inner dimensions: ${win.innerWidth}x${win.innerHeight}`, + `Window outer dimensions: ${win.outerWidth}x${win.outerHeight}`, + `Body scrollWidth: ${win.document.body.scrollWidth}`, + `Body clientWidth: ${win.document.body.clientWidth}`, + `Body offsetWidth: ${win.document.body.offsetWidth}`, + `DocumentElement clientWidth: ${win.document.documentElement.clientWidth}`, + `DocumentElement offsetWidth: ${win.document.documentElement.offsetWidth}`, + `Meta viewport content: ${win.document.querySelector('meta[name="viewport"]')?.getAttribute('content')}`, + `Device Pixel Ratio: ${win.devicePixelRatio}`, + `Cypress Viewport: ${Cypress.config('viewportWidth')}x${Cypress.config('viewportHeight')}`, + ])).to.be.lte(win.innerWidth); + }); + }); +}); diff --git a/tests/e2e/operating-system-selector.cy.ts b/tests/e2e/operating-system-selector.cy.ts new file mode 100644 index 00000000..86a59d55 --- /dev/null +++ b/tests/e2e/operating-system-selector.cy.ts @@ -0,0 +1,69 @@ +import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType'; +import { getEnumValues } from '@/application/Common/Enum'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { getOperatingSystemDisplayName } from '@/presentation/components/Shared/OperatingSystemNames'; +import { AllSupportedOperatingSystems } from '@tests/shared/TestCases/SupportedOperatingSystems'; + +describe('operating system selector', () => { + // Regression test for a bug where switching between operating systems caused uncaught exceptions. + describe('allows user to switch between supported operating systems', () => { + getEnumValues(ViewType).forEach((viewType) => { + it(`switches to ${ViewType[viewType]} view successfully`, () => { + // arrange + cy.visit('/'); + selectViewType(viewType); + getSupportedOperatingSystemsList().forEach((operatingSystem) => { + // act + selectOperatingSystem(operatingSystem); + // assert + assertExpectedActions(); + }); + }); + }); + }); +}); + +function getSupportedOperatingSystemsList() { + /* + Marked: refactor-with-aot-compilation + The operating systems list is hardcoded due to the challenge of loading + the application within Cypress, as its compilation is tightly coupled with Vite. + Ideally, this should dynamically fetch the list from the compiled application. + */ + return AllSupportedOperatingSystems; +} + +function assertExpectedActions() { + /* + Marked: refactor-with-aot-compilation + Assertions are currently hardcoded due to the inability to load the application within + Cypress, as compilation is tightly coupled with Vite. Future refactoring should dynamically + assert the visibility of all actions (e.g., `actions.map((a) => cy.contains(a.title))`) + once the application's compilation process is decoupled from Vite. + */ + cy.contains('Privacy cleanup'); +} + +function selectOperatingSystem(operatingSystem: OperatingSystem) { + const operatingSystemLabel = getOperatingSystemDisplayName(operatingSystem); + if (!operatingSystemLabel) { + throw new Error(`Label does not exist for operating system: ${OperatingSystem[operatingSystem]}`); + } + cy.log(`Visiting operating system: ${operatingSystemLabel}`); + cy + .contains('span', operatingSystemLabel) + .click(); +} + +function selectViewType(viewType: ViewType): void { + const viewTypeLabel = ViewTypeLabels[viewType]; + cy.log(`Selecting view: ${ViewType[viewType]}`); + cy + .contains('span', viewTypeLabel) + .click(); +} + +const ViewTypeLabels: Record = { + [ViewType.Cards]: 'Cards', + [ViewType.Tree]: 'Tree', +} as const; diff --git a/tests/e2e/revert-toggle.cy.ts b/tests/e2e/revert-toggle.cy.ts new file mode 100644 index 00000000..6f2b50df --- /dev/null +++ b/tests/e2e/revert-toggle.cy.ts @@ -0,0 +1,79 @@ +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { openCard } from './support/interactions/card'; + +describe('revert toggle', () => { + context('toggle switch', () => { + beforeEach(() => { + cy.visit('/'); + openCard({ + cardIndex: 1, // first is often cleanup that may lack revert button + }); + cy.get('.toggle-switch') + .filter(':visible') // Avoid side-effects from hidden cards + .first() + .as('toggleSwitch'); + }); + + it('should be visible', () => { + cy.get('@toggleSwitch') + .should('be.visible'); + }); + + it('should have revert label', () => { + cy.get('@toggleSwitch') + .find('span') + .contains('revert', { matchCase: false }); + }); + + it('should render label completely without clipping', () => { // Regression test for a bug where label is partially rendered (clipped) + cy + .get('@toggleSwitch') + .find('span') + .should(($label) => { + const text = $label.text(); + const font = getFont($label[0]); + const expectedMinimumTextWidth = getTextWidth(text, font); + const containerWidth = $label.parent().width(); + expectExists(containerWidth); + expect(expectedMinimumTextWidth).to.be.lessThan(containerWidth, formatAssertionMessage([ + 'Label is not rendered completely.', + `Expected minimum text width: ${expectedMinimumTextWidth}`, + `Actual text container width: ${containerWidth}`, + ])); + }); + }); + + it('should toggle the revert state when clicked', () => { + cy.get('@toggleSwitch').then(($toggleSwitch) => { + // arrange + const initialState = $toggleSwitch.find('.toggle-input').is(':checked'); + + // act + cy.wrap($toggleSwitch).click(); + + // assert + cy.wrap($toggleSwitch).find('.toggle-input').should(($input) => { + const newState = $input.is(':checked'); + expect(newState).to.not.equal(initialState); + }); + }); + }); + }); +}); + +function getFont(element: Element): string { + const computedStyle = window.getComputedStyle(element); + return `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`; +} + +function getTextWidth(text: string, font: string): number { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Unable to get 2D context from canvas element'); + } + ctx.font = font; + const measuredWidth = ctx.measureText(text).width; + return measuredWidth; +} diff --git a/tests/e2e/support/assert/layout-stability.ts b/tests/e2e/support/assert/layout-stability.ts new file mode 100644 index 00000000..59ae2490 --- /dev/null +++ b/tests/e2e/support/assert/layout-stability.ts @@ -0,0 +1,99 @@ +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; + +interface LayoutStabilityTestOptions { + excludeWidth: boolean; + excludeHeight: boolean; +} + +export function assertLayoutStability( + selector: string, + action: ()=> void, + options: Partial | undefined = undefined, +): void { + // arrange + if (options?.excludeWidth === true && options?.excludeHeight === true) { + throw new Error('Invalid test configuration: both width and height exclusions specified.'); + } + let initialMetrics: ViewportMetrics | undefined; + captureViewportMetrics(selector, (metrics) => { + initialMetrics = metrics; + }); + // act + action(); + // assert + captureViewportMetrics(selector, (metrics) => { + const finalMetrics = metrics; + const assertionContext = [ + `Expected (initial metrics before action): ${JSON.stringify(initialMetrics)}`, + `Actual (final metrics after action): ${JSON.stringify(finalMetrics)}`, + ]; + if (options?.excludeWidth !== true) { + expect(initialMetrics?.x).to.equal(finalMetrics.x, formatAssertionMessage([ + 'Width instability detected', + ...assertionContext, + ])); + } + if (options?.excludeHeight !== true) { + expect(initialMetrics?.x).to.equal(finalMetrics.x, formatAssertionMessage([ + 'Height instability detected', + ...assertionContext, + ])); + } + }); +} + +function captureViewportMetrics( + selector: string, + callback: (metrics: ViewportMetrics) => void, +): void { + cy.window().then((win) => { + cy.get(selector) + .then((elements) => { + const element = elements[0]; + const position = getElementViewportMetrics(element, win); + cy.log(`Captured metrics (\`${selector}\`): ${JSON.stringify(position)}`); + callback(position); + }); + }); +} + +interface ViewportMetrics { + readonly x: number; + readonly y: number; + /* + Excluding height and width from the metrics to ensure test accuracy. + Height and width measurements can lead to false negatives due to layout shifts caused by + delayed loading of fonts and icons. + */ +} + +function getElementViewportMetrics(element: HTMLElement, win: Window): ViewportMetrics { + const elementXRelativeToViewport = getElementXRelativeToViewport(element, win); + const elementYRelativeToViewport = getElementYRelativeToViewport(element, win); + return { + x: elementXRelativeToViewport, + y: elementYRelativeToViewport, + }; +} + +function getElementYRelativeToViewport(element: HTMLElement, win: Window): number { + const relativeTop = element.getBoundingClientRect().top; + const { position, top } = win.getComputedStyle(element); + const topValue = position === 'static' ? 0 : parseInt(top, 10); + if (Number.isNaN(topValue)) { + throw new Error(`Could not calculate Y position value from 'top': ${top}`); + } + const viewportRelativeY = relativeTop - topValue + win.scrollY; + return viewportRelativeY; +} + +function getElementXRelativeToViewport(element: HTMLElement, win: Window): number { + const relativeLeft = element.getBoundingClientRect().left; + const { position, left } = win.getComputedStyle(element); + const leftValue = position === 'static' ? 0 : parseInt(left, 10); + if (Number.isNaN(leftValue)) { + throw new Error(`Could not calculate X position value from 'left': ${left}`); + } + const viewportRelativeX = relativeLeft - leftValue + win.scrollX; + return viewportRelativeX; +} diff --git a/tests/e2e/support/commands.ts b/tests/e2e/support/commands.ts new file mode 100644 index 00000000..75a1f461 --- /dev/null +++ b/tests/e2e/support/commands.ts @@ -0,0 +1,2 @@ +// This file is a placeholder to include code to create various custom commands +// and overwrite existing commands, see: https://docs.cypress.io/api/cypress-api/custom-commands diff --git a/tests/e2e/support/e2e.ts b/tests/e2e/support/e2e.ts new file mode 100644 index 00000000..a31706a4 --- /dev/null +++ b/tests/e2e/support/e2e.ts @@ -0,0 +1,30 @@ +/* + This file is processed and loaded automatically before test files. + It's designed to put global configuration and behavior that modifies Cypress. +*/ + +import './commands'; + +// Mitigates a Chrome-specific 'ResizeObserver' error in Cypress tests. +// The 'ResizeObserver loop limit exceeded' error is non-critical but can cause +// false negatives in CI/CD environments, particularly with GitHub runners. +// The issue is absent in actual browser usage and local Cypress testing. +// Community discussions and contributions have led to this handler being +// recommended as a user-level fix rather than a Cypress core inclusion. +// Relevant discussions and attempted core fixes: +// - Original fix suggestion: https://github.com/cypress-io/cypress/issues/8418#issuecomment-992564877 +// - Proposed Cypress core PRs: +// https://github.com/cypress-io/cypress/pull/20257 +// https://github.com/cypress-io/cypress/pull/20284 +// - Current issue tracking: https://github.com/cypress-io/cypress/issues/20341 +// - Related Quasar framework issue: https://github.com/quasarframework/quasar/issues/2233 +// - Chromium bug tracker discussion: https://issues.chromium.org/issues/41369140 +// - Stack Overflow on safely ignoring the error: +// https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded/50387233#50387233 +// https://stackoverflow.com/questions/63653605/resizeobserver-loop-limit-exceeded-api-is-never-used/63653711#63653711 +// - Spec issue related to 'ResizeObserver': https://github.com/WICG/resize-observer/issues/38 +Cypress.on( + 'uncaught:exception', + // Ignore this specific error to prevent false test failures + (err) => !err.message.includes('ResizeObserver loop limit exceeded'), +); diff --git a/tests/e2e/support/interactions/card.ts b/tests/e2e/support/interactions/card.ts new file mode 100644 index 00000000..5894e7e4 --- /dev/null +++ b/tests/e2e/support/interactions/card.ts @@ -0,0 +1,7 @@ +export function openCard(options: { + readonly cardIndex: number; +}) { + cy.get('.card') + .eq(options.cardIndex) + .click(); +} diff --git a/tests/e2e/support/interactions/code-area.ts b/tests/e2e/support/interactions/code-area.ts new file mode 100644 index 00000000..0969978c --- /dev/null +++ b/tests/e2e/support/interactions/code-area.ts @@ -0,0 +1,7 @@ +export function getCurrentHighlightRange() { + return cy + .get('#codeEditor') + .invoke('attr', 'data-test-highlighted-range') + .should('be.a', 'string') + .should('not.equal', ''); +} diff --git a/tests/e2e/support/interactions/header.ts b/tests/e2e/support/interactions/header.ts new file mode 100644 index 00000000..cd7cb084 --- /dev/null +++ b/tests/e2e/support/interactions/header.ts @@ -0,0 +1,3 @@ +export function getHeaderBrandTitle() { + cy.contains('h1', 'privacy.sexy'); +} diff --git a/tests/e2e/support/interactions/script-selection.ts b/tests/e2e/support/interactions/script-selection.ts new file mode 100644 index 00000000..7eae93cb --- /dev/null +++ b/tests/e2e/support/interactions/script-selection.ts @@ -0,0 +1,15 @@ +import { getCurrentHighlightRange } from './code-area'; + +export function selectAllScripts() { + cy.contains('span', 'All') + .click(); + getCurrentHighlightRange() + .should('not.equal', '0'); +} + +export function unselectAllScripts() { + cy.contains('span', 'None') + .click(); + getCurrentHighlightRange() + .should('equal', '0'); +} diff --git a/tests/e2e/support/scenarios/viewport-test-scenarios.ts b/tests/e2e/support/scenarios/viewport-test-scenarios.ts new file mode 100644 index 00000000..57be67b0 --- /dev/null +++ b/tests/e2e/support/scenarios/viewport-test-scenarios.ts @@ -0,0 +1,23 @@ +const SmallScreen: ViewportScenario = { + name: 'iPhone SE', width: 375, height: 667, +}; + +const MediumScreen: ViewportScenario = { + name: '13-inch Laptop', width: 1280, height: 800, +}; + +export const LargeScreen: ViewportScenario = { + name: '4K Ultra HD Desktop', width: 3840, height: 2160, +}; + +export const ViewportTestScenarios: readonly ViewportScenario[] = [ + SmallScreen, + MediumScreen, + LargeScreen, +] as const; + +interface ViewportScenario { + readonly name: string; + readonly width: number; + readonly height: number; +} diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json new file mode 100644 index 00000000..dc85afd1 --- /dev/null +++ b/tests/e2e/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "es5", + "lib": [ + "es5", + "dom" + ], + "types": [ + "cypress", + "node" + ], + "sourceMap": false, + }, + "include": [ + "**/*.ts", + ] +} \ No newline at end of file diff --git a/tests/integration/application/Parser/ApplicationParser.spec.ts b/tests/integration/application/Parser/ApplicationParser.spec.ts new file mode 100644 index 00000000..62d37d46 --- /dev/null +++ b/tests/integration/application/Parser/ApplicationParser.spec.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import { parseApplication } from '@/application/Parser/ApplicationParser'; + +describe('ApplicationParser', () => { + describe('parseApplication', () => { + it('can parse current application', () => { + // act + const act = () => parseApplication(); + // assert + expect(act).to.not.throw(); + }); + }); +}); diff --git a/tests/integration/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.spec.ts b/tests/integration/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.spec.ts new file mode 100644 index 00000000..4c8546d4 --- /dev/null +++ b/tests/integration/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.spec.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import { validateParameterName } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; + +describe('ParameterNameValidator', () => { + describe('accepts when valid', () => { + // arrange + const validValues: readonly string[] = [ + 'lowercase', + 'onlyLetters', + 'l3tt3rsW1thNumb3rs', + ]; + validValues.forEach((validValue) => { + it(validValue, () => { + // act + const act = () => validateParameterName(validValue); + // assert + expect(act).to.not.throw(); + }); + }); + }); + describe('throws if invalid', () => { + // arrange + const testScenarios: readonly { + readonly description: string; + readonly value: string; + }[] = [ + { + description: 'empty name', + value: '', + }, + { + description: 'has @', + value: 'b@d', + }, + { + description: 'has {', + value: 'b{a}d', + }, + ]; + testScenarios.forEach(( + { description, value }, + ) => { + it(description, () => { + // act + const act = () => validateParameterName(value); + // assert + expect(act).to.throw(); + }); + }); + }); +}); diff --git a/tests/integration/composite/DependencyResolution.spec.ts b/tests/integration/composite/DependencyResolution.spec.ts new file mode 100644 index 00000000..d3033fdf --- /dev/null +++ b/tests/integration/composite/DependencyResolution.spec.ts @@ -0,0 +1,54 @@ +import { it, describe, expect } from 'vitest'; +import { inject } from 'vue'; +import { type InjectionKeySelector, InjectionKeys, injectKey } from '@/presentation/injectionSymbols'; +import { provideDependencies } from '@/presentation/bootstrapping/DependencyProvider'; +import { buildContext } from '@/application/Context/ApplicationContextFactory'; +import type { IApplicationContext } from '@/application/Context/IApplicationContext'; +import { executeInComponentSetupContext } from '@tests/shared/Vue/ExecuteInComponentSetupContext'; + +describe('DependencyResolution', () => { + describe('all dependencies can be injected', async () => { + // arrange + const context = await buildContext(); + const dependencies = collectProvidedKeys(context); + Object.values(InjectionKeys).forEach((key) => { + it(`"${key.key.description}"`, () => { + // act + const resolvedDependency = resolve(() => key, dependencies); + // assert + expect(resolvedDependency).toBeDefined(); + }); + }); + }); +}); + +type ProvidedKeys = Record; + +function collectProvidedKeys(context: IApplicationContext): ProvidedKeys { + const providedKeys: ProvidedKeys = {}; + provideDependencies(context, { + inject, + provide: (key, value) => { + providedKeys[key as symbol] = value; + }, + }); + return providedKeys; +} + +function resolve( + selector: InjectionKeySelector, + providedKeys: ProvidedKeys, +): T | undefined { + let injectedDependency: T | undefined; + executeInComponentSetupContext({ + setupCallback: () => { + injectedDependency = injectKey(selector); + }, + mountOptions: { + global: { + provide: providedKeys, + }, + }, + }); + return injectedDependency; +} diff --git a/tests/integration/composite/README.md b/tests/integration/composite/README.md new file mode 100644 index 00000000..26d15f3a --- /dev/null +++ b/tests/integration/composite/README.md @@ -0,0 +1,3 @@ +# Composite Tests + +The `composite` directory contains integration tests that validate the cooperative functionality of multiple components within the application, going beyond unit tests to ensure interconnected parts perform as expected. diff --git a/tests/integration/infrastructure/CodeRunner/ScriptFileCodeRunner.spec.ts b/tests/integration/infrastructure/CodeRunner/ScriptFileCodeRunner.spec.ts new file mode 100644 index 00000000..91f29132 --- /dev/null +++ b/tests/integration/infrastructure/CodeRunner/ScriptFileCodeRunner.spec.ts @@ -0,0 +1,100 @@ +import { mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { exec } from 'node:child_process'; +import { describe, it } from 'vitest'; +import type { ScriptDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/ScriptDirectoryProvider'; +import { ScriptFileCreationOrchestrator } from '@/infrastructure/CodeRunner/Creation/ScriptFileCreationOrchestrator'; +import { ScriptFileCodeRunner } from '@/infrastructure/CodeRunner/ScriptFileCodeRunner'; +import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { LinuxTerminalEmulator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/LinuxVisibleTerminalCommand'; + +describe('ScriptFileCodeRunner', () => { + it('executes simple script correctly', async ({ skip }) => { + // arrange + const currentOperatingSystem = CurrentEnvironment.os; + if (await shouldSkipTest(currentOperatingSystem)) { + skip(); + return; + } + const temporaryDirectoryProvider = createTemporaryDirectoryProvider(); + const codeRunner = createCodeRunner(temporaryDirectoryProvider); + const args = getPlatformSpecificArguments(currentOperatingSystem); + // act + const { success, error } = await codeRunner.runCode(...args); + // assert + expect(success).to.equal(true, formatAssertionMessage([ + 'Failed to successfully execute the script.', + 'Details:', JSON.stringify(error), + ])); + }); +}); + +function getPlatformSpecificArguments( + os: OperatingSystem | undefined, +): Parameters { + switch (os) { + case undefined: + throw new Error('Operating system detection failed: Unable to identify the current platform.'); + case OperatingSystem.Windows: + return [ + [ + '@echo off', + 'echo Hello, World!', + 'exit /b 0', + ].join('\n'), + 'bat', + ]; + case OperatingSystem.macOS: + case OperatingSystem.Linux: + return [ + [ + '#!/bin/bash', + 'echo "Hello, World!"', + 'exit 0', + ].join('\n'), + 'sh', + ]; + default: + throw new Error(`Platform not supported: The current operating system (${os}) is not compatible with this script execution.`); + } +} + +function shouldSkipTest( + os: OperatingSystem | undefined, +): Promise { + if (os !== OperatingSystem.Linux) { + return Promise.resolve(false); + } + return isLinuxTerminalEmulatorSupported(); +} + +function isLinuxTerminalEmulatorSupported(): Promise { + return new Promise((resolve) => { + exec(`which ${LinuxTerminalEmulator}`).on('close', (exitCode) => { + resolve(exitCode !== 0); + }); + }); +} + +function createCodeRunner(directoryProvider: ScriptDirectoryProvider): ScriptFileCodeRunner { + return new ScriptFileCodeRunner( + undefined, + new ScriptFileCreationOrchestrator(undefined, undefined, directoryProvider), + ); +} + +function createTemporaryDirectoryProvider(): ScriptDirectoryProvider { + return { + provideScriptDirectory: async () => { + const temporaryDirectoryPathPrefix = join(tmpdir(), 'privacy-sexy-tests-'); + const temporaryDirectoryFullPath = await mkdtemp(temporaryDirectoryPathPrefix); + return { + success: true, + directoryAbsolutePath: temporaryDirectoryFullPath, + }; + }, + }; +} diff --git a/tests/integration/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.spec.ts b/tests/integration/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.spec.ts new file mode 100644 index 00000000..8982ef32 --- /dev/null +++ b/tests/integration/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.spec.ts @@ -0,0 +1,15 @@ +import { describe, it } from 'vitest'; +import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory'; + +describe('EnvironmentVariablesFactory', () => { + it('can read current environment', () => { + const environmentVariables = EnvironmentVariablesFactory.Current.instance; + Object.entries(environmentVariables).forEach(([key, value]) => { + it(`${key} value is defined`, () => { + expect(value).to.not.equal(undefined); + expect(value).to.not.equal(null); + expect(value).to.not.equal(Number.NaN); + }); + }); + }); +}); diff --git a/tests/integration/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables.spec.ts b/tests/integration/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables.spec.ts new file mode 100644 index 00000000..b3c1543f --- /dev/null +++ b/tests/integration/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables.spec.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import packageJson from '@/../package.json' assert { type: 'json' }; +import type { PropertyKeys } from '@/TypeHelpers'; +import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata'; +import { ViteEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables'; + +describe('ViteEnvironmentVariables', () => { + describe('populates metadata from package.json', () => { + interface ITestCase { + readonly getActualValue: (sut: IAppMetadata) => string; + readonly expected: string; + } + const testCases: { readonly [K in PropertyKeys]: ITestCase } = { + name: { + expected: packageJson.name, + getActualValue: (sut) => sut.name, + }, + version: { + expected: packageJson.version, + getActualValue: (sut) => sut.version, + }, + slogan: { + expected: packageJson.slogan, + getActualValue: (sut) => sut.slogan, + }, + repositoryUrl: { + expected: packageJson.repository.url, + getActualValue: (sut) => sut.repositoryUrl, + }, + homepageUrl: { + expected: packageJson.homepage, + getActualValue: (sut) => sut.homepageUrl, + }, + }; + Object.entries(testCases).forEach(([propertyName, { expected, getActualValue }]) => { + it(`should correctly get the value of ${propertyName}`, () => { + // arrange + const sut = new ViteEnvironmentVariables(); + + // act + const actualValue = getActualValue(sut); + + // assert + expect(actualValue).toBe(expected); + }); + }); + }); +}); diff --git a/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsTestCases.ts b/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsTestCases.ts new file mode 100644 index 00000000..145ee157 --- /dev/null +++ b/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsTestCases.ts @@ -0,0 +1,265 @@ +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { determineTouchSupportOptions } from '@tests/integration/shared/TestCases/TouchSupportOptions'; + +interface BrowserOsTestCase { + readonly userAgent: string; + readonly platformTouchSupport: boolean; + readonly expectedOs: OperatingSystem; +} + +export const BrowserOsTestCases: ReadonlyArray = [ + ...createTests({ + operatingSystem: OperatingSystem.Windows, + userAgents: [ + // Internet Explorer: + 'Mozilla/5.0 (Windows NT 6.3; Win64, x64; Trident/7.0; rv:11.0) like Gecko', + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)', + 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)', + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)', + // Edge (Legacy): + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763', + 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134', + 'Mozilla/5.0 (Windows NT 10.0; WebView/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299', + // Edge (Chromium): + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0', + // Firefox: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', + 'Mozilla/5.0 (Windows NT 6.4; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0', + // Chrome/Brave/QQ Browser: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + // Opera: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 OPR/105.0.0.0', + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 OPR/15.0.1147.100', + 'Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14', + 'Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.10', + 'Opera/9.27 (Windows NT 5.1; U; en)', + 'Opera/9.80 (Windows NT 6.1; Opera Tablet/15165; U; en) Presto/2.8.149 Version/11.1', + // UC Browser: + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 UBrowser/6.0.1308.1016 Safari/537.36', + // Electron: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36', + // jsdom: + 'Mozilla/5.0 (Windows) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/22.1.0', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.macOS, + userAgents: [ + // Firefox: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0', + // Chrome/Brave: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36', + // Safari: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', + // Opera: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 OPR/105.0.0.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.82 Safari/537.36 OPR/29.0.1795.41 (Edition beta)', + // Edge: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0', + // Electron: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36', + // jsdom: + 'Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/22.1.0', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.Linux, + userAgents: [ + // Firefox: + 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0', + // Chrome: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + // Edge: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188', + // Electron: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36', + // jsdom: + 'Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/22.1.0', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.iOS, + userAgents: [ + ...[ // iPhone + // Safari: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', + // Chrome: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/119.0.6045.109 Mobile/15E148 Safari/604.1', + // Firefox: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/1.0 Mobile/12F69 Safari/600.1.4', + // Firefox Focus: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/7.0.4 Mobile/16B91 Safari/605.1.15', + // Opera Mini: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) OPiOS/16.0.15.124050 Mobile/15E148 Safari/9537.53', + // Opera Touch (discontinued): + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.7 Mobile/15E148 Safari/604.1 OPT/4.3.2', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) OPT/3.3.3 Mobile/15E148', + ], + ...[ // iPod + // Safari: + 'Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5', + 'Mozilla/5.0 (iPod; CPU iPhone OS 9_3 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13E233 Safari/601.1', + // Chrome: + 'Mozilla/5.0 (iPod; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/87.0.4280.77 Mobile/15E148 Safari/604.1', + ], + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.iPadOS, + userAgents: [ + /* + `iPad` might be included in user agents on older iPad that's running iOS (not iPadOS), + to avoid additional complexity, we just detect them as iPadOS. + */ + // Safari on iPad (running iOS): + 'Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10', + // Edge on iOS: + 'Mozilla/5.0 (iPad; CPU OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 EdgiOS/46.2.0 Mobile/15E148 Safari/605.1.15', + // Safari on iPad Mini (running iPadOS): + 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.iPadOS, + // Safari on (≥ iPadOS 13) and iPhone on desktop mode reports user agents identical to macOS + userAgents: [ + // Safari: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10156) AppleWebKit/605.1.15 (KHTML, like Gecko)', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.ChromeOS, + userAgents: [ + // Chrome: + 'Mozilla/5.0 (X11; CrOS x86_64 11316.165.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.122 Safari/537.36', + 'Mozilla/5.0 (X11; CrOS armv7l 4537.56.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.38 Safari/537.36', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.Android, + userAgents: [ + // Opera Mini: + 'Opera/9.80 (Android; Opera Mini/32.0/88.150; U; sr) Presto/2.12 Version/12.16', + 'Opera/9.80 (Android; Opera Mini/8.0.1807/36.1609; U; en) Presto/2.12.423 Version/12.16', + 'Opera/9.80 (Android 2.2; Opera Mobi/-2118645896; U; pl) Presto/2.7.60 Version/10.5', + // Chrome: + 'Mozilla/5.0 (Linux; Android 4.4.4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 Mobile OPR/15.0.1147.100', + 'Mozilla/5.0 (Linux; Android 9; SM-G960U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 6.0; CAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 4.2.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Safari/537.36', + 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/46.0.2490.76 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; ONEPLUS A6003) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Mobile Safari/537.36', + // Firefox: + 'Mozilla/5.0 (Android 4.4; Tablet; rv:41.0) Gecko/41.0 Firefox/41.0', + 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0', + 'Mozilla/5.0 (Android; Mobile; rv:40.0) Gecko/40.0 Firefox/40.0', + 'Mozilla/5.0 (Android; Tablet; rv:40.0) Gecko/40.0 Firefox/40.0', + // Firefox Focus: + 'Mozilla/5.0 (Linux; Android 7.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/1.0 Chrome/59.0.3029.83 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 7.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/1.0 Chrome/59.0.3029.83 Safari/537.36', + 'Mozilla/5.0 (Android 7.0; Mobile; rv:62.0) Gecko/62.0 Firefox/62.0', + // Firefox Klar (german edition): + 'Mozilla/5.0 (Linux; Android 7.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Klar/1.0 Chrome/58.0.3029.83 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 7.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/4.1 Chrome/62.0.3029.83 Mobile Safari/537.36', + 'Mozilla/5.0 (Android 7.0; Mobile; rv:62.0) Gecko/62.0 Firefox/62.0', + // UC Browser: + 'Mozilla/5.0 (Linux; U; Android 6.0; en-US; CPH1609 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.10.2.1164 Mobile Safari/537.36', + 'UCWEB/2.0 (Linux; U; Adr 5.1; en-US; Lenovo Z90a40 Build/LMY47O) U2/1.0.0 UCBrowser/11.1.5.890 U2/1.0.0 Mobile', + 'Mozilla/5.0 (Linux; U; Android 5.1; en-US; Lenovo Z90a40 Build/LMY47O) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/11.1.5.890 U3/0.8.0 Mobile Safari/534.30', + 'Mozilla/5.0 (Linux; U; Android 2.3; zh-CN; MI-ONEPlus) AppleWebKit/534.13 (KHTML, like Gecko) UCBrowser/8.6.0.199 U3/0.8.0 Mobile Safari/534.13', + 'UCWEB/2.0 (Linux; U; Adr 2.3; en-US; MI-ONEPlus) U2/1.0.0 UCBrowser/8.6.0.199 U2/1.0.0 Mobile', + // Opera: + 'Mozilla/5.0 (Linux; Android 2.3.4; MT11i Build/4.0.2.A.0.62) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.123 Mobile Safari/537.22 OPR/14.0.1025.52315', + // Opera Touch (discontinued): + 'Mozilla/5.0 (Linux; Android 8.1.0; BBF100-6 Build/OPM1.171019.026) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/68.0.3440.91 Mobile Safari/537.36 OPT/6B8575B', + // Samsung Browser: + 'Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-G965F Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.0 Chrome/67.0.3396.87 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 8.0.0; SAMSUNG SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/8.2 Chrome/63.0.3239.111 Mobile Safari/537.36', + // QQ Browser: + 'Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; vivo X21A Build/OPM1.171019.011) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/9.1 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-URL-Manager Mobile Safari/537.36', + // Vivo Browser: + 'Mozilla/5.0 (Linux; Android 10; V1990A; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36 VivoBrowser/10.3.10.0', + // Android Generic Webkit based: + 'Mozilla/5.0 (Linux; U; Android 4.4.4; pt-br; SM-G530BT Build/KTU84P) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + 'Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; Q40; Android/4.4.2; Release/12.15.2015) AppleWebKit/534.30 (KHTML, like Gecko) Mobile Safari/534.30', + 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.BlackBerry10, + userAgents: [ + // BlackBerry Browser: + 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.1.0.1429 Mobile Safari/537.10+', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.BlackBerryTabletOS, + userAgents: [ + // BlackBerry Browser: + 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.0.0; en-US) AppleWebKit/535.8+ (KHTML, like Gecko) Version/7.2.0.0 Safari/535.8+', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.BlackBerryOS, + userAgents: [ + // BlackBerry Browser: + 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.466 Mobile Safari/534.8+', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.WindowsPhone, + userAgents: [ + // Internet Explorer Mobile: + 'Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 625) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537', + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)', + 'Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; id313-3) like Gecko', + 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; NOKIA; Lumia 900)', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.Windows10Mobile, + userAgents: [ + // Chrome: + 'Mozilla/5.0 (Windows Mobile 13; Android 10.0; Microsoft; Lumia 950XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36 Safari/537.36', + // Opera Mini: + 'Opera/9.80 (Windows Mobile; Opera Mini/103.1.21595/25.657; U; en) Presto/2.5.25 Version/10.54', + // Edge 100: + 'Mozilla/5.0 (compatible; Android 10.0;SM-G973F; Windows Mobile 10.0; Chrome/106.0.5249.126 ) AppleWebKit/535.1 (KHTML, like Gecko) EdgA/100/0.1185.50 Mobile Safari/535.1 3gpp-gba', + // Edge legacy: + 'Mozilla/5.0 (Windows Phone 10.0; Android 5.1.1; NOKIA; Lumia 1520) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/13.10586', + // Firefox: + 'Mozilla/5.0 (Windows Phone 10.0; Mobile; rv:107.0) Gecko/107.0 Firefox/107.0', + ], + }), + ...createTests({ + operatingSystem: OperatingSystem.KaiOS, + userAgents: [ + // Firefox: + 'Mozilla/5.0 (Mobile; LYF/F90M/LYF_F90M_000-03-12-110119; Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5', + ], + }), +]; + +function createTests(testScenario: { + readonly operatingSystem: OperatingSystem, + readonly userAgents: readonly string[], +}): BrowserOsTestCase[] { + return determineTouchSupportOptions(testScenario.operatingSystem) + .flatMap((hasTouch): readonly BrowserOsTestCase[] => testScenario + .userAgents.map((userAgent): BrowserOsTestCase => ({ + userAgent, + platformTouchSupport: hasTouch, + expectedOs: testScenario.operatingSystem, + }))); +} diff --git a/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/ConditionBasedOsDetector.spec.ts b/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/ConditionBasedOsDetector.spec.ts new file mode 100644 index 00000000..b7c2d0e4 --- /dev/null +++ b/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/ConditionBasedOsDetector.spec.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { ConditionBasedOsDetector } from '@/infrastructure/RuntimeEnvironment/Browser/BrowserOs/ConditionBasedOsDetector'; +import type { BrowserEnvironment } from '@/infrastructure/RuntimeEnvironment/Browser/BrowserOs/BrowserOsDetector'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { BrowserOsTestCases } from './BrowserOsTestCases'; + +describe('ConditionBasedOsDetector', () => { + describe('detect', () => { + it('detects as expected', () => { + BrowserOsTestCases.forEach((testCase) => { + // arrange + const sut = new ConditionBasedOsDetector(); + const environment: BrowserEnvironment = { + userAgent: testCase.userAgent, + isTouchSupported: testCase.platformTouchSupport, + }; + // act + const actual = sut.detect(environment); + // assert + expect(actual).to.equal(testCase.expectedOs, formatAssertionMessage([ + `Expected: "${OperatingSystem[testCase.expectedOs]}"`, + `Actual: "${actual === undefined ? 'undefined' : OperatingSystem[actual]}"`, + `User agent: "${testCase.userAgent}"`, + `Touch support: "${testCase.platformTouchSupport ? 'Yes, supported' : 'No, unsupported.'}"`, + ])); + }); + }); + }); +}); diff --git a/tests/integration/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.spec.ts b/tests/integration/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.spec.ts new file mode 100644 index 00000000..10beea20 --- /dev/null +++ b/tests/integration/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory'; + +describe('RuntimeEnvironmentFactory', () => { + describe('CurrentEnvironment', () => { + it('identifies as browser in test environment', () => { // Ensures test independence from Electron IPC + // arrange + const expectedDesktopAppState = false; + // act + const isRunningAsDesktop = CurrentEnvironment.isRunningAsDesktopApplication; + // assert + expect(isRunningAsDesktop).to.equal(expectedDesktopAppState); + }); + it('identifies as non-production in test environment', () => { + // arrange + const expectedNonProductionState = true; + // act + const isNonProductionEnvironment = CurrentEnvironment.isNonProduction; + // assert + expect(isNonProductionEnvironment).to.equal(expectedNonProductionState); + }); + }); +}); diff --git a/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts b/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts new file mode 100644 index 00000000..53630653 --- /dev/null +++ b/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts @@ -0,0 +1,63 @@ +import { describe } from 'vitest'; +import type { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions'; +import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks'; +import { isBoolean } from '@/TypeHelpers'; + +describe('SanityChecks', () => { + describe('validateRuntimeSanity', () => { + describe('does not throw on current environment', () => { + // arrange + const testOptions = generateTestOptions(); + testOptions.forEach((options) => { + it(`options: ${JSON.stringify(options)}`, () => { + // act + const act = () => validateRuntimeSanity(options); + + // assert + expect(act).to.not.throw(); + }); + }); + }); + }); +}); + +function generateTestOptions(): ISanityCheckOptions[] { + const defaultOptions: ISanityCheckOptions = { + validateEnvironmentVariables: true, + validateWindowVariables: true, + }; + return generateBooleanPermutations(defaultOptions); +} + +function generateBooleanPermutations(object: T | undefined): T[] { + if (!object) { + return []; + } + + const keys = Object.keys(object) as (keyof T)[]; + + if (keys.length === 0) { + return [object]; + } + + const currentKey = keys[0]; + const currentValue = object[currentKey]; + + if (!isBoolean(currentValue)) { + return generateBooleanPermutations({ + ...object, + [currentKey]: currentValue, + }); + } + + const remainingKeys = Object.fromEntries( + keys.slice(1).map((key) => [key, object[key]]), + ) as unknown as T | undefined; + + const subPermutations = generateBooleanPermutations(remainingKeys); + + return [ + ...subPermutations.map((p) => ({ ...p, [currentKey]: true })), + ...subPermutations.map((p) => ({ ...p, [currentKey]: false })), + ]; +} diff --git a/tests/integration/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.spec.ts b/tests/integration/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.spec.ts new file mode 100644 index 00000000..a786261a --- /dev/null +++ b/tests/integration/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.spec.ts @@ -0,0 +1,7 @@ +import { describe } from 'vitest'; +import { EnvironmentVariablesValidator } from '@/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator'; +import { itNoErrorsOnCurrentEnvironment } from './ValidatorTestRunner'; + +describe('EnvironmentVariablesValidator', () => { + itNoErrorsOnCurrentEnvironment(() => new EnvironmentVariablesValidator()); +}); diff --git a/tests/integration/infrastructure/RuntimeSanity/Validators/ValidatorTestRunner.ts b/tests/integration/infrastructure/RuntimeSanity/Validators/ValidatorTestRunner.ts new file mode 100644 index 00000000..3cb00ee5 --- /dev/null +++ b/tests/integration/infrastructure/RuntimeSanity/Validators/ValidatorTestRunner.ts @@ -0,0 +1,15 @@ +import { it, expect } from 'vitest'; +import type { ISanityValidator } from '@/infrastructure/RuntimeSanity/Common/ISanityValidator'; + +export function itNoErrorsOnCurrentEnvironment( + factory: () => ISanityValidator, +) { + it('it does report errors on current environment', () => { + // arrange + const validator = factory(); + // act + const errors = [...validator.collectErrors()]; + // assert + expect(errors).to.have.lengthOf(0); + }); +} diff --git a/tests/integration/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.spec.ts b/tests/integration/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.spec.ts new file mode 100644 index 00000000..624f6b38 --- /dev/null +++ b/tests/integration/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.spec.ts @@ -0,0 +1,7 @@ +import { describe } from 'vitest'; +import { WindowVariablesValidator } from '@/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator'; +import { itNoErrorsOnCurrentEnvironment } from './ValidatorTestRunner'; + +describe('WindowVariablesValidator', () => { + itNoErrorsOnCurrentEnvironment(() => new WindowVariablesValidator()); +}); diff --git a/tests/integration/presentation/bootstrapping/ApplicationBootstrapper.spec.ts b/tests/integration/presentation/bootstrapping/ApplicationBootstrapper.spec.ts new file mode 100644 index 00000000..b74010b0 --- /dev/null +++ b/tests/integration/presentation/bootstrapping/ApplicationBootstrapper.spec.ts @@ -0,0 +1,16 @@ +import { describe, it } from 'vitest'; +import { createApp } from 'vue'; +import { ApplicationBootstrapper } from '@/presentation/bootstrapping/ApplicationBootstrapper'; +import { expectDoesNotThrowAsync } from '@tests/shared/Assertions/ExpectThrowsAsync'; + +describe('ApplicationBootstrapper', () => { + it('can bootstrap without errors', async () => { + // arrange + const sut = new ApplicationBootstrapper(); + const vueApp = createApp({}); + // act + const act = () => sut.bootstrap(vueApp); + // assert + await expectDoesNotThrowAsync(act); + }); +}); diff --git a/tests/integration/presentation/bootstrapping/Modules/MobileSafariActivePseudoClassEnabler.spec.ts b/tests/integration/presentation/bootstrapping/Modules/MobileSafariActivePseudoClassEnabler.spec.ts new file mode 100644 index 00000000..38b34fed --- /dev/null +++ b/tests/integration/presentation/bootstrapping/Modules/MobileSafariActivePseudoClassEnabler.spec.ts @@ -0,0 +1,66 @@ +import { describe, it, afterEach } from 'vitest'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { MobileSafariActivePseudoClassEnabler } from '@/presentation/bootstrapping/Modules/MobileSafariActivePseudoClassEnabler'; +import { createEventSpies } from '@tests/shared/Spies/EventTargetSpy'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { isTouchEnabledDevice } from '@/infrastructure/RuntimeEnvironment/Browser/TouchSupportDetection'; +import { BrowserRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/Browser/BrowserRuntimeEnvironment'; +import { MobileSafariDetectionTestCases } from './MobileSafariDetectionTestCases'; + +describe('MobileSafariActivePseudoClassEnabler', () => { + describe('bootstrap', () => { + MobileSafariDetectionTestCases.forEach(({ + description, userAgent, supportsTouch, expectedResult, + }) => { + it(description, () => { + // arrange + const expectedEvent: keyof WindowEventMap = 'touchstart'; + patchUserAgent(userAgent, afterEach); + const { isAddEventCalled, formatListeners } = createEventSpies(window, afterEach); + const patchedEnvironment = new TouchSupportControlledBrowserEnvironment(supportsTouch); + const sut = new MobileSafariActivePseudoClassEnabler(patchedEnvironment); + // act + sut.bootstrap(); + // assert + const isSet = isAddEventCalled(expectedEvent); + expect(isSet).to.equal(expectedResult, formatAssertionMessage([ + `Expected result\t\t: ${expectedResult ? 'true (mobile Safari)' : 'false (not mobile Safari)'}`, + `Actual result\t\t: ${isSet ? 'true (mobile Safari)' : 'false (not mobile Safari)'}`, + `User agent\t\t: ${navigator.userAgent}`, + `Touch supported\t\t: ${supportsTouch}`, + `Current OS\t\t: ${patchedEnvironment.os === undefined ? 'unknown' : OperatingSystem[patchedEnvironment.os]}`, + `Is desktop?\t\t: ${patchedEnvironment.isRunningAsDesktopApplication ? 'Yes (Desktop app)' : 'No (Browser)'}`, + `Listeners\t\t: ${formatListeners()}`, + ])); + }); + }); + }); +}); + +function patchUserAgent( + userAgent: string, + restoreCallback: (restoreFunc: () => void) => void, +) { + const originalNavigator = window.navigator; + const userAgentGetter = { get: () => userAgent }; + window.navigator = Object.create(navigator, { + userAgent: userAgentGetter, + }); + restoreCallback(() => { + Object.assign(window, { + navigator: originalNavigator, + }); + }); +} + +function getTouchDetectorMock( + isTouchEnabled: boolean, +): typeof isTouchEnabledDevice { + return () => isTouchEnabled; +} + +class TouchSupportControlledBrowserEnvironment extends BrowserRuntimeEnvironment { + public constructor(isTouchEnabled: boolean) { + super(window, undefined, undefined, getTouchDetectorMock(isTouchEnabled)); + } +} diff --git a/tests/integration/presentation/bootstrapping/Modules/MobileSafariDetectionTestCases.ts b/tests/integration/presentation/bootstrapping/Modules/MobileSafariDetectionTestCases.ts new file mode 100644 index 00000000..58f6a0b1 --- /dev/null +++ b/tests/integration/presentation/bootstrapping/Modules/MobileSafariDetectionTestCases.ts @@ -0,0 +1,221 @@ +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { determineTouchSupportOptions } from '@tests/integration/shared/TestCases/TouchSupportOptions'; + +interface PlatformTestCase { + readonly description: string; + readonly userAgent: string; + readonly supportsTouch: boolean; + readonly expectedResult: boolean; +} + +export const MobileSafariDetectionTestCases: ReadonlyArray = [ + ...createBrowserTestCases({ + browserName: 'Safari', + expectedResult: true, + userAgents: [ + { + deviceInfo: 'Safari on iPad (≥ 13)', + operatingSystem: OperatingSystem.iPadOS, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', // same as macOS (desktop) + supportsTouch: true, + }, + { + deviceInfo: 'Safari on iPad (< 13)', + operatingSystem: OperatingSystem.iPadOS, + userAgent: 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B14 3 Safari/601.1', + }, + { + deviceInfo: 'Safari on iPhone', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1', + }, + { + deviceInfo: 'Safari on iPod touch', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Mozila/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Geckto) Version/3.0 Mobile/3A101a Safari/419.3', + // https://web.archive.org/web/20231112165804/https://www.cnet.com/tech/mobile/safari-for-ipod-touch-has-different-user-agent-string-may-not-go-directly-to-iphone-optimized-sites/null/ + }, + ], + }), + ...createBrowserTestCases({ + browserName: 'Safari', + expectedResult: false, + userAgents: [ + { + deviceInfo: 'Safari on macOS', + operatingSystem: OperatingSystem.macOS, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', + supportsTouch: false, + }, + ], + }), + ...createBrowserTestCases({ + browserName: 'Chrome', + expectedResult: false, + userAgents: [ + { + deviceInfo: 'macOS', + operatingSystem: OperatingSystem.macOS, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + }, + { + deviceInfo: 'macOS (Electron)', + operatingSystem: OperatingSystem.macOS, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36', + }, + { + deviceInfo: 'iPad (iPadOS 17)', + operatingSystem: OperatingSystem.iPadOS, + userAgent: 'Mozilla/5.0 (iPad; CPU OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/119.0.6045.109 Mobile/15E148 Safari/604.1', + }, + { + deviceInfo: 'iPhone (iOS 17)', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/119.0.6045.109 Mobile/15E148 Safari/604.1', + }, + { + deviceInfo: 'iPhone (iOS 12)', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/70.0.3538.75 Mobile/15E148 Safari/605.1', + }, + { + deviceInfo: 'iPod Touch (iOS 12)', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Mozilla/5.0 (iPod; CPU iPhone OS 12_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/86.0.4240.93 Mobile/15E148 Safari/604.1', + }, + ], + }), + ...createBrowserTestCases({ + browserName: 'Firefox', + expectedResult: false, + userAgents: [ + { + deviceInfo: 'macOS', + operatingSystem: OperatingSystem.macOS, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14.1; rv:119.0) Gecko/20100101 Firefox/119.0', + }, + { + deviceInfo: 'iPad (iPadOS 13)', + operatingSystem: OperatingSystem.iPadOS, + userAgent: 'Mozilla/5.0 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/19.1b16203 Mobile/15E148 Safari/605.1.15', + }, + { + deviceInfo: 'iPhone (iOS 17)', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/117.2 Mobile/15E148 Safari/605.1.15', + }, + ], + }), + ...createBrowserTestCases({ + browserName: 'Edge', + expectedResult: false, + userAgents: [ + { + deviceInfo: 'macOS', + operatingSystem: OperatingSystem.macOS, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55', + }, + { + deviceInfo: 'iPad (iPadOS 15)', + operatingSystem: OperatingSystem.iPadOS, + userAgent: 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/96.0.1054.61 Version/15.0 Mobile/15E148 Safari/604.1', + }, + { + deviceInfo: 'iPhone (iOS 17)', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/118.0.2088.81 Version/17.0 Mobile/15E148 Safari/604.1', + }, + ], + }), + ...createBrowserTestCases({ + browserName: 'Opera', + expectedResult: false, + userAgents: [ + { + deviceInfo: 'Opera Mini on iPhone', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Opera/9.80 (iPhone; Opera Mini/5.0.0176/764; U; en) Presto/2.4.15', + // https://web.archive.org/web/20140221034354/http://my.opera.com/haavard/blog/2010/04/16/iphone-user-agent + }, + { + deviceInfo: 'Opera Mini (Opera Turbo) on iPhone', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) OPiOS/8.0.0.78129 Mobile/11D201 Safari/9537.53', + // https://web.archive.org/web/20231112164709/https://dev.opera.com/blog/opera-mini-8-for-ios/ + }, + { + deviceInfo: 'Opera on macOS', + operatingSystem: OperatingSystem.macOS, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 OPR/94.0.0.0', + // https://web.archive.org/web/20231112164741/https://forums.opera.com/topic/59600/have-the-user-agent-browser-identification-match-with-the-mac-os-version-the-browser-is-running-on + }, + { + deviceInfo: 'Opera on macOS (legacy)', + operatingSystem: OperatingSystem.macOS, + userAgent: 'Opera/9.80 (Macintosh; Intel Mac OS X 10.8; U; ru) Presto/2.10 Version/12.00', + // https://web.archive.org/web/20231112164741/https://forums.opera.com/topic/59600/have-the-user-agent-browser-identification-match-with-the-mac-os-version-the-browser-is-running-on + }, + { + deviceInfo: 'Opera Touch on iPhone', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) OPT/3.3.3 Mobile/15E148', + }, + { + deviceInfo: 'Opera Mini on iPhone', + operatingSystem: OperatingSystem.iOS, + userAgent: 'Opera/9.80 (iPhone; Opera Mini/14.0.0/37.8603; U; en) Presto/2.12.423 Version/12.16', + }, + { + deviceInfo: 'Opera Mini on iPad', + operatingSystem: OperatingSystem.iPadOS, + userAgent: 'Opera/9.80 (iPad; Opera Mini/7.0.5/191.320; U; id) Presto/2.12.423 Version/12.16', + }, + ], + }), + ...createBrowserTestCases({ + browserName: 'Vivo Browser', // Runs only Vivo (Android) devices + expectedResult: false, + userAgents: [ + { + deviceInfo: 'VivoBrowser on Android', + operatingSystem: OperatingSystem.Android, + userAgent: 'Mozilla/5.0 (Linux; Android 10; V1990A; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36 VivoBrowser/10.3.10.0', + }, + ], + }), +]; + +interface UserAgentTestScenario { + readonly userAgent: string; + readonly operatingSystem: OperatingSystem; + readonly deviceInfo: string; + readonly supportsTouch?: boolean; +} + +interface BrowserTestScenario { + readonly browserName: string; + readonly expectedResult: boolean; + readonly userAgents: readonly UserAgentTestScenario[]; +} + +function createBrowserTestCases( + scenario: BrowserTestScenario, +): PlatformTestCase[] { + return scenario.userAgents.flatMap((agentInfo): readonly PlatformTestCase[] => { + const touchCases = agentInfo.supportsTouch === undefined + ? determineTouchSupportOptions(agentInfo.operatingSystem) + : [agentInfo.supportsTouch]; + return touchCases.map((hasTouch): PlatformTestCase => ({ + description: [ + scenario.expectedResult ? '[POSITIVE]' : '[NEGATIVE]', + scenario.browserName, + OperatingSystem[agentInfo.operatingSystem], + agentInfo.deviceInfo, + hasTouch === true ? '[TOUCH]' : '[NO TOUCH]', + ].join(' | '), + userAgent: agentInfo.userAgent, + supportsTouch: hasTouch, + expectedResult: scenario.expectedResult, + })); + }); +} diff --git a/tests/integration/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer.spec.ts b/tests/integration/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer.spec.ts new file mode 100644 index 00000000..b0e161ab --- /dev/null +++ b/tests/integration/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer.spec.ts @@ -0,0 +1,244 @@ +import { describe, it, expect } from 'vitest'; +import { parseApplication } from '@/application/Parser/ApplicationParser'; +import { CompositeMarkdownRenderer } from '@/presentation/components/Scripts/View/Tree/NodeContent/Markdown/CompositeMarkdownRenderer'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { parseHtml } from '@tests/shared/HtmlParser'; + +describe('CompositeMarkdownRenderer', () => { + describe('can render all docs', () => { + // arrange + const renderer = new CompositeMarkdownRenderer(); + for (const node of collectAllDocumentedExecutables()) { + it(`${node.executableLabel}`, () => { + // act + const html = renderer.render(node.docs); + const result = analyzeHtmlContentForCorrectFormatting(html); + // assert + expect(result.isCorrectlyFormatted).to.equal(true, formatAssertionMessage([ + 'HTML validation failed', + `Executable Label: ${node.executableLabel}`, + `Generated HTML: ${result.generatedHtml}`, + ])); + }); + } + }); + it('should convert plain URLs to hyperlinks and apply markdown formatting', () => { + // arrange + const renderer = new CompositeMarkdownRenderer(); + const expectedPlainUrl = 'https://privacy.sexy'; + const expectedLabel = 'privacy.sexy'; + const markdownContent = `Visit ${expectedPlainUrl} for privacy scripts.`; + + // act + const renderedOutput = renderer.render(markdownContent); + + // assert + const links = extractHyperlinksFromHtmlContent(renderedOutput); + assertExpectedNumberOfHyperlinksInContent({ + links, expectedLength: 1, markdownContent, renderedOutput, + }); + assertHyperlinkWithExpectedLabelUrlAndAttributes({ + link: links[0], expectedHref: expectedPlainUrl, expectedLabel, + }); + }); + it('should correctly handle inline reference labels converting them to superscript', () => { + // arrange + const renderer = new CompositeMarkdownRenderer(); + const expectedInlineReferenceUrlHref = 'https://undergroundwires.dev'; + const expectedInlineReferenceUrlLabel = '1'; + const markdownContent = [ + `See reference [${expectedInlineReferenceUrlLabel}].`, + '\n', + `[${expectedInlineReferenceUrlLabel}]: ${expectedInlineReferenceUrlHref} "Example"`, + ].join('\n'); + + // act + const renderedOutput = renderer.render(markdownContent); + + // assert + assertSuperscriptReference({ + renderedOutput, + markdownContent, + expectedHref: expectedInlineReferenceUrlHref, + expectedLabel: expectedInlineReferenceUrlLabel, + }); + }); + it('should process mixed content, converting URLs and references within complex Markdown', () => { + // arrange + const renderer = new CompositeMarkdownRenderer(); + const expectedInlineReferenceUrlHref = 'https://undergroundwires.dev'; + const expectedInlineReferenceUrlLabel = 'Example Reference'; + const expectedPlainUrlHref = 'https://privacy.sexy'; + const expectedPlainUrlLabel = 'privacy.sexy'; + const markdownContent = [ + `This is a test of [inline references][${expectedInlineReferenceUrlLabel}] and plain URLs ${expectedPlainUrlHref}`, + '\n', + `[${expectedInlineReferenceUrlLabel}]: ${expectedInlineReferenceUrlHref} "Example"`, + ].join('\n'); + + // act + const renderedOutput = renderer.render(markdownContent); + + // assert + const links = extractHyperlinksFromHtmlContent(renderedOutput); + assertExpectedNumberOfHyperlinksInContent({ + links, expectedLength: 2, markdownContent, renderedOutput, + }); + assertHyperlinkWithExpectedLabelUrlAndAttributes({ + link: links[0], + expectedHref: expectedInlineReferenceUrlHref, + expectedLabel: expectedInlineReferenceUrlLabel, + }); + assertHyperlinkWithExpectedLabelUrlAndAttributes({ + link: links[1], + expectedHref: expectedPlainUrlHref, + expectedLabel: expectedPlainUrlLabel, + }); + assertSuperscriptReference({ + renderedOutput, + markdownContent, + expectedHref: expectedInlineReferenceUrlHref, + expectedLabel: expectedInlineReferenceUrlLabel, + }); + }); + it('ensures no
tags are inserted for single line breaks', () => { + // arrange + const renderer = new CompositeMarkdownRenderer(); + const markdownContent = 'Line 1\nLine 2'; + + // act + const renderedOutput = renderer.render(markdownContent); + + // assert + expect(renderedOutput).not.to.include('
', formatAssertionMessage([ + 'Expected no
tags for single line breaks', + `Rendered content: ${renderedOutput}`, + ])); + }); + it('applies default anchor attributes for all links including dynamically converted ones', () => { + // arrange + const renderer = new CompositeMarkdownRenderer(); + const markdownContent = '[Example](https://example.com) and https://privacy.sexy.'; + + // act + const renderedOutput = renderer.render(markdownContent); + + // assert + const links = extractHyperlinksFromHtmlContent(renderedOutput); + assertExpectedNumberOfHyperlinksInContent({ + links, expectedLength: 2, markdownContent, renderedOutput, + }); + Array.from(links).forEach((link) => { + assertHyperlinkOpensInNewTabWithSecureRelAttributes({ link }); + }); + }); +}); + +function assertExpectedNumberOfHyperlinksInContent(context: { + readonly links: HTMLAnchorElement[]; + readonly expectedLength: number; + readonly markdownContent: string; + readonly renderedOutput: string; +}): void { + expect(context.links.length).to.equal(context.expectedLength, formatAssertionMessage([ + `Expected exactly "${context.expectedLength}" hyperlinks in the rendered output`, + `Found ${context.links.length} hyperlinks instead.`, + `Markdown content: ${context.markdownContent}`, + `Rendered output: ${context.renderedOutput}`, + ])); +} + +function assertHyperlinkWithExpectedLabelUrlAndAttributes(context: { + readonly link: HTMLAnchorElement; + readonly expectedHref: string; + readonly expectedLabel: string; +}): void { + expect(context.link.href).to.include(context.expectedHref, formatAssertionMessage([ + 'The hyperlink href does not match the expected URL', + `Expected URL: ${context.expectedHref}`, + `Actual URL: ${context.link.href}`, + ])); + expect(context.link.textContent).to.equal(context.expectedLabel, formatAssertionMessage([ + `Expected text content of the hyperlink to be ${context.expectedLabel}`, + `Actual text content: ${context.link.textContent}`, + ])); + assertHyperlinkOpensInNewTabWithSecureRelAttributes({ link: context.link }); +} + +function assertHyperlinkOpensInNewTabWithSecureRelAttributes(context: { + readonly link: HTMLAnchorElement; +}): void { + expect(context.link.target).to.equal('_blank', formatAssertionMessage([ + 'Expected the hyperlink to open in new tabs (target="_blank")', + `Actual target attribute of a link: ${context.link.target}`, + ])); + expect(context.link.rel).to.include('noopener noreferrer', formatAssertionMessage([ + 'Expected the hyperlink to have rel="noopener noreferrer" for security', + `Actual rel attribute of a link: ${context.link.rel}`, + ])); +} + +function assertSuperscriptReference(context: { + readonly renderedOutput: string; + readonly markdownContent: string; + readonly expectedHref: string; + readonly expectedLabel: string; +}): void { + const html = parseHtml(context.renderedOutput); + const superscript = html.getElementsByTagName('sup')[0]; + expectExists(superscript, formatAssertionMessage([ + 'Expected at least single superscript.', + `Rendered content does not contain any superscript: ${context.renderedOutput}`, + `Markdown content: ${context.markdownContent}`, + ])); + const links = extractHyperlinksFromHtmlContent(superscript.innerHTML); + assertExpectedNumberOfHyperlinksInContent({ + links, + expectedLength: 1, + markdownContent: context.markdownContent, + renderedOutput: context.renderedOutput, + }); + assertHyperlinkWithExpectedLabelUrlAndAttributes({ + link: links[0], + expectedHref: context.expectedHref, + expectedLabel: context.expectedLabel, + }); +} + +function extractHyperlinksFromHtmlContent(htmlText: string): HTMLAnchorElement[] { + const html = parseHtml(htmlText); + const links = html.getElementsByTagName('a'); + return Array.from(links); +} + +interface DocumentedExecutable { + readonly executableLabel: string + readonly docs: string +} + +function collectAllDocumentedExecutables(): DocumentedExecutable[] { + const app = parseApplication(); + const allExecutables = app.collections.flatMap((collection) => [ + ...collection.getAllScripts(), + ...collection.getAllCategories(), + ]); + const allDocumentedExecutables = allExecutables.filter((e) => e.docs.length > 0); + return allDocumentedExecutables.map((executable): DocumentedExecutable => ({ + executableLabel: `${executable.name} (${executable.executableId})`, + docs: executable.docs.join('\n'), + })); +} + +interface HTMLValidationResult { + readonly isCorrectlyFormatted: boolean; + readonly generatedHtml: string; +} + +function analyzeHtmlContentForCorrectFormatting(value: string): HTMLValidationResult { + const doc = parseHtml(value); + return { + isCorrectlyFormatted: Array.from(doc.body.childNodes).some((node) => node.nodeType === 1), + generatedHtml: doc.body.innerHTML, + }; +} diff --git a/tests/integration/presentation/components/Scripts/View/Tree/Shared/Icon/UseSvgLoader.spec.ts b/tests/integration/presentation/components/Scripts/View/Tree/Shared/Icon/UseSvgLoader.spec.ts new file mode 100644 index 00000000..21e10a2b --- /dev/null +++ b/tests/integration/presentation/components/Scripts/View/Tree/Shared/Icon/UseSvgLoader.spec.ts @@ -0,0 +1,20 @@ +import { + describe, it, expect, +} from 'vitest'; +import { IconNames } from '@/presentation/components/Shared/Icon/IconName'; +import { useSvgLoader } from '@/presentation/components/Shared/Icon/UseSvgLoader'; +import { waitForValueChange } from '@tests/shared/Vue/WaitForValueChange'; + +describe('useSvgLoader', () => { + describe('can load all SVGs', () => { + for (const iconName of IconNames) { + it(iconName, async () => { + // act + const { svgContent } = useSvgLoader(() => iconName); + await waitForValueChange(svgContent); + // assert + expect(svgContent.value).toBeTruthy(); + }); + } + }); +}); diff --git a/tests/integration/presentation/components/Scripts/View/Tree/Shared/Modal/Hooks/UseEscapeKeyListener.spec.ts b/tests/integration/presentation/components/Scripts/View/Tree/Shared/Modal/Hooks/UseEscapeKeyListener.spec.ts new file mode 100644 index 00000000..7209b91f --- /dev/null +++ b/tests/integration/presentation/components/Scripts/View/Tree/Shared/Modal/Hooks/UseEscapeKeyListener.spec.ts @@ -0,0 +1,74 @@ +import { + describe, it, expect, +} from 'vitest'; +import { defineComponent } from 'vue'; +import { mount } from '@vue/test-utils'; +import { createEventSpies } from '@tests/shared/Spies/EventTargetSpy'; +import { useEscapeKeyListener } from '@/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; + +const EventSource: EventTarget = document; +type EventName = keyof DocumentEventMap; + +describe('UseEscapeKeyListener', () => { + it('executes the callback when the Escape key is pressed', () => { + // arrange + const expectedCallbackCall = true; + let callbackCalled = false; + const callback = () => { + callbackCalled = true; + }; + + // act + mountWrapperComponent(callback); + const event = new KeyboardEvent('keyup', { key: 'Escape' }); + EventSource.dispatchEvent(event); + + // assert + expect(callbackCalled).to.equal(expectedCallbackCall); + }); + + it('adds the event listener on component mount', () => { + // arrange + const expectedEventType: EventName = 'keyup'; + const { isAddEventCalled, formatListeners } = createEventSpies(EventSource, afterEach); + + // act + mountWrapperComponent(); + + // assert + const isAdded = isAddEventCalled(expectedEventType); + expect(isAdded).to.equal(true, formatAssertionMessage([ + `Expected event type to be added: "${expectedEventType}".`, + `Current listeners: ${formatListeners()}`, + ])); + }); + + it('removes the event listener once unmounted', () => { + // arrange + const expectedEventType: EventName = 'keyup'; + const { isRemoveEventCalled, formatListeners } = createEventSpies(EventSource, afterEach); + + // act + const wrapper = mountWrapperComponent(); + wrapper.unmount(); + + // assert + const isRemoved = isRemoveEventCalled(expectedEventType); + expect(isRemoved).to.equal(true, formatAssertionMessage([ + `Expected event type to be removed: "${expectedEventType}".`, + `Remaining listeners: ${formatListeners()}`, + ])); + }); +}); + +function mountWrapperComponent( + callback = () => {}, +) { + return mount(defineComponent({ + setup() { + useEscapeKeyListener(callback); + }, + template: '

', + })); +} diff --git a/tests/integration/presentation/components/Scripts/View/Tree/Shared/OperatingSystemNames.spec.ts b/tests/integration/presentation/components/Scripts/View/Tree/Shared/OperatingSystemNames.spec.ts new file mode 100644 index 00000000..115ce5bd --- /dev/null +++ b/tests/integration/presentation/components/Scripts/View/Tree/Shared/OperatingSystemNames.spec.ts @@ -0,0 +1,24 @@ +import { + describe, it, expect, +} from 'vitest'; +import { ApplicationFactory } from '@/application/ApplicationFactory'; +import { getOperatingSystemDisplayName } from '@/presentation/components/Shared/OperatingSystemNames'; +import { OperatingSystem } from '@/domain/OperatingSystem'; + +describe('OperatingSystemNames', () => { + describe('getOperatingSystemDisplayName', () => { + describe('retrieving display names for supported operating systems', async () => { + // arrange + const application = await ApplicationFactory.Current.getApp(); + const supportedOperatingSystems = application.getSupportedOsList(); + supportedOperatingSystems.forEach((supportedOperatingSystem) => { + it(`should return a non-empty name for ${OperatingSystem[supportedOperatingSystem]}`, () => { + // act + const displayName = getOperatingSystemDisplayName(supportedOperatingSystem); + // assert + expect(displayName).to.have.length.greaterThanOrEqual(1); + }); + }); + }); + }); +}); diff --git a/tests/integration/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState.spec.ts b/tests/integration/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState.spec.ts new file mode 100644 index 00000000..5bab859c --- /dev/null +++ b/tests/integration/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState.spec.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { useKeyboardInteractionState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/UseKeyboardInteractionState'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { executeInComponentSetupContext } from '@tests/shared/Vue/ExecuteInComponentSetupContext'; + +describe('useKeyboardInteractionState', () => { + describe('isKeyboardBeingUsed', () => { + it('`false` initially', () => { + // arrange + const expectedValue = false; + // act + const { returnObject } = mountWrapperComponent(); + // assert + const actualValue = returnObject.isKeyboardBeingUsed.value; + expect(actualValue).to.equal(expectedValue); + }); + + it('`true` after any key is pressed', () => { + // arrange + const expectedValue = true; + // act + const { returnObject } = mountWrapperComponent(); + triggerKeyPress(); + // assert + const actualValue = returnObject.isKeyboardBeingUsed.value; + expect(actualValue).to.equal(expectedValue); + }); + + it('`false` after any key is pressed once detached', () => { + // arrange + const expectedValue = false; + // act + const { wrapper, returnObject } = mountWrapperComponent(); + wrapper.unmount(); + triggerKeyPress(); // should not react to it + // assert + const actualValue = returnObject.isKeyboardBeingUsed.value; + expect(actualValue).to.equal(expectedValue); + }); + }); +}); + +function triggerKeyPress() { + const eventSource: EventTarget = document; + const keydownEvent = new KeyboardEvent('keydown', { key: 'a' }); + eventSource.dispatchEvent(keydownEvent); +} + +function mountWrapperComponent() { + let returnObject: ReturnType | undefined; + const wrapper = executeInComponentSetupContext({ + setupCallback: () => { + returnObject = useKeyboardInteractionState(); + }, + disableAutoUnmount: true, + }); + expectExists(returnObject); + return { + returnObject, + wrapper, + }; +} diff --git a/tests/integration/presentation/components/Scripts/View/Tree/TreeView/TreeView.spec.ts b/tests/integration/presentation/components/Scripts/View/Tree/TreeView/TreeView.spec.ts new file mode 100644 index 00000000..4fcc5fa9 --- /dev/null +++ b/tests/integration/presentation/components/Scripts/View/Tree/TreeView/TreeView.spec.ts @@ -0,0 +1,158 @@ +import { + describe, it, expect, +} from 'vitest'; +import { mount } from '@vue/test-utils'; +import { defineComponent, shallowRef } from 'vue'; +import TreeView from '@/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue'; +import type { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData'; +import { provideDependencies } from '@/presentation/bootstrapping/DependencyProvider'; +import { ApplicationContextStub } from '@tests/unit/shared/Stubs/ApplicationContextStub'; + +describe('TreeView', () => { + it('renders all provided root nodes correctly', async () => { + // arrange + const nodes = createSampleNodes(); + const { wrapper } = mountWrapperComponent({ + initialNodeData: nodes, + }); + + // act + await waitForStableDom(wrapper.element); + + // assert + const expectedTotalRootNodes = nodes.length; + expect(wrapper.findAll('.node').length).to.equal(expectedTotalRootNodes, wrapper.html()); + const rootNodeTexts = nodes.map((node) => (node.data as TreeInputMetadata).label); + rootNodeTexts.forEach((label) => { + expect(wrapper.text()).to.include(label); + }); + }); + + // Regression test for a bug where updating the nodes prop caused uncaught exceptions. + it('updates nodes correctly when props change', async () => { + // arrange + const firstNodeLabel = 'Node 1'; + const secondNodeLabel = 'Node 2'; + const initialNodes: TreeInputNodeDataWithMetadata[] = [{ id: 'node1', data: { label: firstNodeLabel } }]; + const updatedNodes: TreeInputNodeDataWithMetadata[] = [{ id: 'node2', data: { label: secondNodeLabel } }]; + const { wrapper, nodes } = mountWrapperComponent({ + initialNodeData: initialNodes, + }); + + // act + nodes.value = updatedNodes; + await waitForStableDom(wrapper.element); + + // assert + expect(wrapper.text()).toContain(secondNodeLabel); + expect(wrapper.text()).not.toContain(firstNodeLabel); + }); +}); + +function mountWrapperComponent(options?: { + readonly initialNodeData?: readonly TreeInputNodeDataWithMetadata[], +}) { + const nodes = shallowRef(options?.initialNodeData ?? createSampleNodes()); + const wrapper = mount(defineComponent({ + components: { + TreeView, + }, + setup() { + provideDependencies(new ApplicationContextStub()); + + const selectedLeafNodeIds = shallowRef([]); + + return { + nodes, + selectedLeafNodeIds, + }; + }, + template: ` + + + + `, + })); + return { + wrapper, + nodes, + }; +} + +interface TreeInputMetadata { + readonly label: string; +} + +type TreeInputNodeDataWithMetadata = TreeInputNodeData & { readonly data?: TreeInputMetadata }; + +function createSampleNodes(): TreeInputNodeDataWithMetadata[] { + return [ + { + id: 'root1', + data: { + label: 'Root 1', + }, + children: [ + { + id: 'child1', + data: { + label: 'Child 1', + }, + }, + { + id: 'child2', + data: { + label: 'Child 2', + }, + }, + ], + }, + { + id: 'root2', + data: { + label: 'Root 2', + }, + children: [ + { + id: 'child3', + data: { + label: 'Child 3', + }, + }, + ], + }, + ]; +} + +function waitForStableDom(rootElement, timeout = 3000, interval = 200): Promise { + return new Promise((resolve, reject) => { + let lastTimeoutId: ReturnType; + const observer = new MutationObserver(() => { + if (lastTimeoutId) { + clearTimeout(lastTimeoutId); + } + + lastTimeoutId = setTimeout(() => { + observer.disconnect(); + resolve(); + }, interval); + }); + + observer.observe(rootElement, { + attributes: true, + childList: true, + subtree: true, + characterData: true, + }); + + setTimeout(() => { + observer.disconnect(); + reject(new Error('Timeout waiting for DOM to stabilize')); + }, timeout); + }); +} diff --git a/tests/integration/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.spec.ts b/tests/integration/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.spec.ts new file mode 100644 index 00000000..37605d97 --- /dev/null +++ b/tests/integration/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.spec.ts @@ -0,0 +1,67 @@ +import { + describe, it, expect, +} from 'vitest'; +import { mount } from '@vue/test-utils'; +import { defineComponent } from 'vue'; +import { useAutoUnsubscribedEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener'; + +describe('UseAutoUnsubscribedEventListener', () => { + describe('event listening on different targets', () => { + const testCases: readonly { + readonly description: string; + readonly eventTarget: EventTarget; + }[] = [ + { + description: 'a div element', + eventTarget: document.createElement('div'), + }, + { + description: 'the document', + eventTarget: document, + }, + { + description: 'the HTML element', + eventTarget: document.documentElement, + }, + { + description: 'the body element', + eventTarget: document.body, + }, + // `window` target is not working in tests due to how `jsdom` handles it + ]; + testCases.forEach(( + { description, eventTarget }, + ) => { + it(description, () => { + // arrange + let actualEvent: KeyboardEvent | undefined; + const expectedEvent = new KeyboardEvent('keypress'); + mountWrapperComponent( + ({ startListening }) => { + startListening(eventTarget, 'keypress', (event) => { + actualEvent = event; + }); + }, + ); + + // act + eventTarget.dispatchEvent(expectedEvent); + + // assert + expect(actualEvent).to.equal(expectedEvent); + }); + }); + }); +}); + +function mountWrapperComponent( + callback: (returnObject: ReturnType) => void, +) { + return mount(defineComponent({ + setup() { + const returnObject = useAutoUnsubscribedEventListener(); + callback(returnObject); + }, + template: '
', + })); +} diff --git a/tests/integration/presentation/components/Shared/Modal/ModalContainer.spec.ts b/tests/integration/presentation/components/Shared/Modal/ModalContainer.spec.ts new file mode 100644 index 00000000..5119ab0c --- /dev/null +++ b/tests/integration/presentation/components/Shared/Modal/ModalContainer.spec.ts @@ -0,0 +1,29 @@ +import { + describe, it, expect, +} from 'vitest'; +import { mount } from '@vue/test-utils'; +import ModalContainer from '@/presentation/components/Shared/Modal/ModalContainer.vue'; + +describe('ModalContainer', () => { + it('closes on pressing ESC key', async () => { + // arrange + const wrapper = mountComponent({ modelValue: true }); + + // act + const escapeEvent = new KeyboardEvent('keyup', { key: 'Escape' }); + document.dispatchEvent(escapeEvent); + + // assert + expect(wrapper.emitted('update:modelValue')).to.deep.equal([[false]]); + }); +}); + +function mountComponent(options: { + readonly modelValue: boolean, +}) { + return mount(ModalContainer, { + props: { + modelValue: options.modelValue, + }, + }); +} diff --git a/tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts b/tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts new file mode 100644 index 00000000..679aa7eb --- /dev/null +++ b/tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts @@ -0,0 +1,15 @@ +import { it, describe, expect } from 'vitest'; +import { connectApisWithContextBridge } from '@/presentation/electron/preload/ContextBridging/ApiContextBridge'; + +describe('ApiContextBridge', () => { + describe('connectApisWithContextBridge', () => { + it('can provide keys and values', () => { + // arrange + const bridgeConnector = () => {}; + // act + const act = () => connectApisWithContextBridge(bridgeConnector); + // assert + expect(act).to.not.throw(); + }); + }); +}); diff --git a/tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts b/tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts new file mode 100644 index 00000000..29364b01 --- /dev/null +++ b/tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts @@ -0,0 +1,66 @@ +import { it, describe, expect } from 'vitest'; +import { provideWindowVariables } from '@/presentation/electron/preload/ContextBridging/RendererApiProvider'; +import { + isArray, isBoolean, isFunction, isNullOrUndefined, isNumber, isPlainObject, isString, +} from '@/TypeHelpers'; + +describe('RendererApiProvider', () => { + describe('provideWindowVariables', () => { + describe('conforms to Electron\'s context bridging requirements', () => { + // https://www.electronjs.org/docs/latest/api/context-bridge + const variables = provideWindowVariables(); + Object.entries(variables).forEach(([key, value]) => { + it(`\`${key}\` conforms to allowed types for context bridging`, () => { + // act + const act = () => checkAllowedType(value); + // assert + expect(act).to.not.throw(); + }); + }); + }); + }); +}); + +function checkAllowedType(value: unknown): void { + if (isBasicType(value)) { + return; + } + if (isArray(value)) { + checkArrayElements(value); + return; + } + if (!isPlainObject(value)) { + throw new Error(`Type error: Expected a valid object, array, or primitive type, but received type '${typeof value}'.`); + } + if (isNullOrUndefined(value)) { + throw new Error('Type error: Value is null or undefined, which is not allowed.'); + } + checkObjectProperties(value); +} + +function isBasicType(value: unknown): boolean { + return isString(value) || isNumber(value) || isBoolean(value) || isFunction(value); +} + +function checkArrayElements(array: unknown[]): void { + array.forEach((item, index) => { + try { + checkAllowedType(item); + } catch (error) { + throw new Error(`Invalid array element at index ${index}: ${error.message}`); + } + }); +} + +function checkObjectProperties(obj: NonNullable): void { + if (Object.keys(obj).some((key) => !isString(key))) { + throw new Error('Type error: At least one object key is not a string, which violates the allowed types.'); + } + Object.entries(obj).forEach(([key, memberValue]) => { + try { + checkAllowedType(memberValue); + } catch (error) { + throw new Error(`Invalid object property '${key}': ${error.message}`); + } + }); +} diff --git a/tests/integration/shared/TestCases/TouchSupportOptions.ts b/tests/integration/shared/TestCases/TouchSupportOptions.ts new file mode 100644 index 00000000..ef92a2f1 --- /dev/null +++ b/tests/integration/shared/TestCases/TouchSupportOptions.ts @@ -0,0 +1,37 @@ +import { OperatingSystem } from '@/domain/OperatingSystem'; + +enum TouchSupportState { + AlwaysSupported, + MayBeSupported, + NeverSupported, +} + +const TouchSupportPerOperatingSystem: Record = { + [OperatingSystem.Android]: TouchSupportState.AlwaysSupported, + [OperatingSystem.iOS]: TouchSupportState.AlwaysSupported, + [OperatingSystem.iPadOS]: TouchSupportState.AlwaysSupported, + [OperatingSystem.ChromeOS]: TouchSupportState.AlwaysSupported, + [OperatingSystem.KaiOS]: TouchSupportState.MayBeSupported, + [OperatingSystem.BlackBerry10]: TouchSupportState.AlwaysSupported, + [OperatingSystem.BlackBerryOS]: TouchSupportState.AlwaysSupported, + [OperatingSystem.BlackBerryTabletOS]: TouchSupportState.AlwaysSupported, + [OperatingSystem.WindowsPhone]: TouchSupportState.AlwaysSupported, + [OperatingSystem.Windows10Mobile]: TouchSupportState.AlwaysSupported, + [OperatingSystem.Windows]: TouchSupportState.MayBeSupported, + [OperatingSystem.Linux]: TouchSupportState.MayBeSupported, + [OperatingSystem.macOS]: TouchSupportState.NeverSupported, // Consider Touch Bar as a special case +}; + +export function determineTouchSupportOptions(os: OperatingSystem): boolean[] { + const state = TouchSupportPerOperatingSystem[os]; + switch (state) { + case TouchSupportState.AlwaysSupported: + return [true]; + case TouchSupportState.MayBeSupported: + return [true, false]; + case TouchSupportState.NeverSupported: + return [false]; + default: + throw new Error(`Unknown state: ${TouchSupportState[state]}`); + } +} diff --git a/tests/shared/Assertions/ExpectDeepIncludes.ts b/tests/shared/Assertions/ExpectDeepIncludes.ts new file mode 100644 index 00000000..65e551d0 --- /dev/null +++ b/tests/shared/Assertions/ExpectDeepIncludes.ts @@ -0,0 +1,47 @@ +import { indentText } from '@/application/Common/Text/IndentText'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; + +/** + * Asserts that an array deeply includes a specified item by comparing JSON-serialized versions. + * Designed to be used as the Chai methods 'to.deep.include' and 'to.deep.contain' do not work. + */ +export function expectDeepIncludes( + arrayToSearch: readonly T[], + expectedItem: T, +) { + const serializedItemsFromArray = arrayToSearch.map((c) => jsonSerializeForComparison(c)); + const serializedExpectedItem = jsonSerializeForComparison(expectedItem); + expect(serializedItemsFromArray).to.include(serializedExpectedItem, formatAssertionMessage([ + 'Mismatch in expected items.', + 'The provided array does not include the expected item.', + 'Expected item:', + indentText(serializeItemForDisplay(expectedItem)), + `Provided items (total: ${arrayToSearch.length}):`, + indentText(serializeArrayForDisplay(arrayToSearch)), + ])); +} + +function jsonSerializeForComparison(obj: unknown): string { + return JSON.stringify(obj); +} + +function serializeArrayForDisplay(array: readonly T[]): string { + return array.map((item) => indentText(serializeItemForDisplay(item))).join('\n-\n'); +} + +function serializeItemForDisplay(item: unknown): string { + const typeDescription = getTypeDescription(item); + const jsonSerializedItem = JSON.stringify(item, null, 2); + return `${typeDescription}\n${jsonSerializedItem}`; +} + +function getTypeDescription(item: unknown): string { + // Basic type detection using typeof + let type = typeof item; + // More specific type detection for object types using Object.prototype.toString + if (type === 'object') { + const preciseType = Object.prototype.toString.call(item); + type = preciseType.replace(/^\[object (\S+)\]$/, '$1'); + } + return `Type: ${type}`; +} diff --git a/tests/shared/Assertions/ExpectDeepThrowsError.ts b/tests/shared/Assertions/ExpectDeepThrowsError.ts new file mode 100644 index 00000000..99315b32 --- /dev/null +++ b/tests/shared/Assertions/ExpectDeepThrowsError.ts @@ -0,0 +1,51 @@ +import { expect } from 'vitest'; +import { expectExists } from './ExpectExists'; + +// `toThrowError` does not assert the error type (https://github.com/vitest-dev/vitest/blob/v0.34.2/docs/api/expect.md#tothrowerror) +export function expectDeepThrowsError(delegate: () => void, expected: T) { + // arrange + let actual: T | undefined; + // act + try { + delegate(); + } catch (error) { + actual = error; + } + // assert + expectExists(actual); + expect(Boolean(actual.stack)).to.equal(true, 'Empty stack trace.'); + expect(expected.message).to.equal(actual.message); + expect(expected.name).to.equal(actual.name); + expectDeepEqualsIgnoringUndefined(expected, actual); +} + +function expectDeepEqualsIgnoringUndefined( + expected: object | undefined, + actual: object | undefined, +) { + const actualClean = removeUndefinedProperties(actual); + const expectedClean = removeUndefinedProperties(expected); + expect(expectedClean).to.deep.equal(actualClean); +} + +function removeUndefinedProperties(obj: object | undefined): object | undefined { + if (!obj) { + return obj; + } + return Object.keys(obj).reduce((acc, key) => { + const value = obj[key]; + switch (typeof value) { + case 'object': { + const cleanValue = removeUndefinedProperties(value); // recurse + if (!cleanValue || !Object.keys(cleanValue).length) { + return { ...acc }; + } + return { ...acc, [key]: cleanValue }; + } + case 'undefined': + return { ...acc }; + default: + return { ...acc, [key]: value }; + } + }, {}); +} diff --git a/tests/shared/Assertions/ExpectExists.ts b/tests/shared/Assertions/ExpectExists.ts new file mode 100644 index 00000000..5e0c54ee --- /dev/null +++ b/tests/shared/Assertions/ExpectExists.ts @@ -0,0 +1,18 @@ +/** + * Asserts that the provided value is neither null nor undefined. + * + * This function is a more explicit alternative to `expect(value).to.exist` in test cases. + * It is particularly useful in situations where TypeScript's control flow analysis + * does not recognize standard assertions, ensuring that `value` is treated as + * non-nullable in the subsequent code. This can prevent potential type errors + * and enhance code safety and clarity. + */ +export function expectExists(value: T, errorMessage?: string): asserts value is NonNullable { + if (value === null || value === undefined) { + throw new Error([ + 'Assertion failed: expected value is either null or undefined.', + 'Expected a non-null and non-undefined value.', + ...(errorMessage ? [errorMessage] : []), + ].join('\n')); + } +} diff --git a/tests/shared/Assertions/ExpectThrowsAsync.ts b/tests/shared/Assertions/ExpectThrowsAsync.ts new file mode 100644 index 00000000..176728bd --- /dev/null +++ b/tests/shared/Assertions/ExpectThrowsAsync.ts @@ -0,0 +1,29 @@ +import { expect } from 'vitest'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; + +export async function expectThrowsAsync( + method: () => Promise, + errorMessage: string, +) { + let error: Error | undefined; + try { + await method(); + } catch (err) { + error = err; + } + expectExists(error); + expect(error).to.be.an(Error.name); + expect(error.message).to.equal(errorMessage); +} + +export async function expectDoesNotThrowAsync( + method: () => Promise, +) { + let error: Error | undefined; + try { + await method(); + } catch (err) { + error = err; + } + expect(error).toBeUndefined(); +} diff --git a/tests/shared/Assertions/ExpectTrue.ts b/tests/shared/Assertions/ExpectTrue.ts new file mode 100644 index 00000000..bdc2e0df --- /dev/null +++ b/tests/shared/Assertions/ExpectTrue.ts @@ -0,0 +1,18 @@ +/** + * Asserts that the provided boolean value is true. + * + * Useful when TypeScript's control flow analysis does not recognize standard + * assertions, ensuring `value` is treated as true in subsequent code. This helps + * prevent type errors and improves code safety and clarity. An optional custom + * error message can be provided for more detailed assertion failures. + */ +export function expectTrue(value: boolean, errorMessage?: string): asserts value is true { + if (value !== true) { + throw new Error([ + `Assertion failed: Expected true, received ${value.toString()}.`, + 'Assertion failed: expected value is not true.', + ...(typeof value !== 'boolean' ? [`Received type: ${typeof value}`] : []), + ...(errorMessage ? [errorMessage] : []), + ].join('\n')); + } +} diff --git a/tests/shared/FormatAssertionMessage.ts b/tests/shared/FormatAssertionMessage.ts new file mode 100644 index 00000000..f50ed4fd --- /dev/null +++ b/tests/shared/FormatAssertionMessage.ts @@ -0,0 +1,7 @@ +export function formatAssertionMessage(lines: readonly string[]) { + return [ // Using many newlines so `vitest` output looks good + '\n---', + ...lines, + '---\n\n', + ].join('\n'); +} diff --git a/tests/shared/HtmlParser.ts b/tests/shared/HtmlParser.ts new file mode 100644 index 00000000..eb28fba8 --- /dev/null +++ b/tests/shared/HtmlParser.ts @@ -0,0 +1,5 @@ +export function parseHtml(htmlString: string): Document { + const parser = new window.DOMParser(); + const htmlDoc = parser.parseFromString(htmlString, 'text/html'); + return htmlDoc; +} diff --git a/tests/shared/Spies/EventTargetSpy.ts b/tests/shared/Spies/EventTargetSpy.ts new file mode 100644 index 00000000..57cf4fc3 --- /dev/null +++ b/tests/shared/Spies/EventTargetSpy.ts @@ -0,0 +1,70 @@ +export function createEventSpies( + eventTarget: EventTarget, + restoreCallback: (restoreFunc: () => void) => void, +) { + const originalAddEventListener = eventTarget.addEventListener; + const originalRemoveEventListener = eventTarget.removeEventListener; + + const currentListeners = new Array>(); + + const addEventListenerCalls = new Array>(); + const removeEventListenerCalls = new Array>(); + + eventTarget.addEventListener = ( + ...args: Parameters + ): ReturnType => { + addEventListenerCalls.push(args); + currentListeners.push(args); + return originalAddEventListener.call(eventTarget, ...args); + }; + + eventTarget.removeEventListener = ( + ...args: Parameters + ): ReturnType => { + removeEventListenerCalls.push(args); + const [type, listener] = args; + const registeredListener = findCurrentListener(type, listener); + if (registeredListener) { + const index = currentListeners.indexOf(registeredListener); + if (index > -1) { + currentListeners.splice(index, 1); + } + } + return originalRemoveEventListener.call(eventTarget, ...args); + }; + + function findCurrentListener( + type: string, + listener: EventListenerOrEventListenerObject | null, + ): Parameters | undefined { + return currentListeners.find((args) => { + const [eventType, eventListener] = args; + return eventType === type && listener === eventListener; + }); + } + + restoreCallback(() => { + eventTarget.addEventListener = originalAddEventListener; + eventTarget.removeEventListener = originalRemoveEventListener; + }); + + return { + isAddEventCalled(eventType: string): boolean { + const call = addEventListenerCalls.find((args) => { + const [type] = args; + return type === eventType; + }); + return call !== undefined; + }, + isRemoveEventCalled(eventType: string) { + const call = removeEventListenerCalls.find((args) => { + const [type] = args; + return type === eventType; + }); + return call !== undefined; + }, + formatListeners: () => { + return JSON.stringify(currentListeners); + }, + }; +} diff --git a/tests/shared/TestCases/SupportedOperatingSystems.ts b/tests/shared/TestCases/SupportedOperatingSystems.ts new file mode 100644 index 00000000..45ed4143 --- /dev/null +++ b/tests/shared/TestCases/SupportedOperatingSystems.ts @@ -0,0 +1,11 @@ +import { OperatingSystem } from '@/domain/OperatingSystem'; + +export type SupportedOperatingSystem = OperatingSystem.Windows +| OperatingSystem.Linux +| OperatingSystem.macOS; + +export const AllSupportedOperatingSystems: readonly OperatingSystem[] = [ + OperatingSystem.Windows, + OperatingSystem.Linux, + OperatingSystem.macOS, +] as const; diff --git a/tests/shared/Vue/ExecuteInComponentSetupContext.ts b/tests/shared/Vue/ExecuteInComponentSetupContext.ts new file mode 100644 index 00000000..532d28f2 --- /dev/null +++ b/tests/shared/Vue/ExecuteInComponentSetupContext.ts @@ -0,0 +1,27 @@ +import { shallowMount, type ComponentMountingOptions } from '@vue/test-utils'; +import { defineComponent } from 'vue'; + +type MountOptions = ComponentMountingOptions; + +/** + * A test helper utility that provides a component `setup()` context. + * This function allows running code that depends on Vue lifecycle hooks, + * such as `onMounted`, within a component's `setup` function. + */ +export function executeInComponentSetupContext(options: { + readonly setupCallback: () => void; + readonly disableAutoUnmount?: boolean; + readonly mountOptions?: MountOptions, +}): ReturnType { + const componentWrapper = shallowMount(defineComponent({ + setup() { + options.setupCallback(); + }, + // Component requires a template or render function + template: '
Test Component: setup context
', + }), options.mountOptions); + if (!options.disableAutoUnmount) { + componentWrapper.unmount(); // Ensure cleanup of callback tasks + } + return componentWrapper; +} diff --git a/tests/shared/Vue/WaitForValueChange.ts b/tests/shared/Vue/WaitForValueChange.ts new file mode 100644 index 00000000..3f43bca5 --- /dev/null +++ b/tests/shared/Vue/WaitForValueChange.ts @@ -0,0 +1,20 @@ +import { type WatchSource, watch } from 'vue'; + +export function waitForValueChange( + valueWatcher: WatchSource, + timeoutMs = 2000, +): Promise { + return new Promise((resolve, reject) => { + const unwatch = watch(valueWatcher, (newValue, oldValue) => { + if (newValue !== oldValue) { + unwatch(); + resolve(newValue); + } + }, { immediate: false }); + + setTimeout(() => { + unwatch(); + reject(new Error('Timeout waiting for value to change.')); + }, timeoutMs); + }); +} diff --git a/tests/shared/bootstrap/BlobPolyfill.ts b/tests/shared/bootstrap/BlobPolyfill.ts new file mode 100644 index 00000000..824174d7 --- /dev/null +++ b/tests/shared/bootstrap/BlobPolyfill.ts @@ -0,0 +1,7 @@ +import { Blob as BlobPolyfill } from 'node:buffer'; + +export function polyfillBlob() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.Blob = BlobPolyfill as any; + // Workaround as `blob.text()` is not available in jsdom (https://github.com/jsdom/jsdom/issues/2555) +} diff --git a/tests/shared/bootstrap/FailTestOnConsoleError.ts b/tests/shared/bootstrap/FailTestOnConsoleError.ts new file mode 100644 index 00000000..7267fef8 --- /dev/null +++ b/tests/shared/bootstrap/FailTestOnConsoleError.ts @@ -0,0 +1,23 @@ +import { + beforeEach, afterEach, vi, expect, +} from 'vitest'; +import type { FunctionKeys } from '@/TypeHelpers'; + +export function failTestOnConsoleError() { + const consoleMethodsToCheck: readonly FunctionKeys[] = [ + 'warn', + 'error', + ]; + + beforeEach(() => { + consoleMethodsToCheck.forEach((methodName) => { + vi.spyOn(console, methodName).mockClear(); + }); + }); + + afterEach(() => { + consoleMethodsToCheck.forEach((methodName) => { + expect(console[methodName]).not.toHaveBeenCalled(); + }); + }); +} diff --git a/tests/shared/bootstrap/setup.ts b/tests/shared/bootstrap/setup.ts new file mode 100644 index 00000000..4c25f7a0 --- /dev/null +++ b/tests/shared/bootstrap/setup.ts @@ -0,0 +1,8 @@ +import { afterEach } from 'vitest'; +import { enableAutoUnmount } from '@vue/test-utils'; +import { polyfillBlob } from './BlobPolyfill'; +import { failTestOnConsoleError } from './FailTestOnConsoleError'; + +enableAutoUnmount(afterEach); +polyfillBlob(); +failTestOnConsoleError(); diff --git a/tests/unit/application/ApplicationFactory.spec.ts b/tests/unit/application/ApplicationFactory.spec.ts new file mode 100644 index 00000000..1b167f52 --- /dev/null +++ b/tests/unit/application/ApplicationFactory.spec.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { ApplicationFactory, type ApplicationGetterType } from '@/application/ApplicationFactory'; +import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub'; + +describe('ApplicationFactory', () => { + describe('getApp', () => { + it('returns result from the getter', async () => { + // arrange + const expected = new ApplicationStub(); + const getter: ApplicationGetterType = () => expected; + const sut = new SystemUnderTest(getter); + // act + const actual = await Promise.all([ + sut.getApp(), + sut.getApp(), + sut.getApp(), + sut.getApp(), + ]); + // assert + expect(actual.every((value) => value === expected)); + }); + it('only executes getter once', async () => { + // arrange + let totalExecution = 0; + const expected = new ApplicationStub(); + const getter: ApplicationGetterType = () => { + totalExecution++; + return expected; + }; + const sut = new SystemUnderTest(getter); + // act + await Promise.all([ + sut.getApp(), + sut.getApp(), + sut.getApp(), + sut.getApp(), + ]); + // assert + expect(totalExecution).to.equal(1); + }); + }); +}); + +class SystemUnderTest extends ApplicationFactory { + public constructor(costlyGetter: ApplicationGetterType) { + super(costlyGetter); + } +} diff --git a/tests/unit/application/Common/Array.ComparerTestScenario.ts b/tests/unit/application/Common/Array.ComparerTestScenario.ts new file mode 100644 index 00000000..a300aadf --- /dev/null +++ b/tests/unit/application/Common/Array.ComparerTestScenario.ts @@ -0,0 +1,74 @@ +interface IComparerTestCase { + readonly name: string; + readonly first: readonly T[]; + readonly second: readonly T[]; + readonly expected: boolean; +} + +export class ComparerTestScenario { + private readonly testCases: Array> = []; + + public addEmptyArrays(expectedResult: boolean) { + return this.addTestCase({ + name: 'empty array', + first: [], + second: [], + expected: expectedResult, + }, true); + } + + public addSameItemsWithSameOrder(expectedResult: boolean) { + return this.addTestCase({ + name: 'same items with same order', + first: [1, 2, 3], + second: [1, 2, 3], + expected: expectedResult, + }, true); + } + + public addSameItemsWithDifferentOrder(expectedResult: boolean) { + return this.addTestCase({ + name: 'same items with different order', + first: [1, 2, 3], + second: [2, 3, 1], + expected: expectedResult, + }, true); + } + + public addDifferentItemsWithSameLength(expectedResult: boolean) { + return this.addTestCase({ + name: 'different items with same length', + first: [1, 2, 3], + second: [4, 5, 6], + expected: expectedResult, + }, true); + } + + public addDifferentItemsWithDifferentLength(expectedResult: boolean) { + return this.addTestCase({ + name: 'different items with different length', + first: [1, 2], + second: [3, 4, 5], + expected: expectedResult, + }, true); + } + + public forEachCase(handler: (testCase: IComparerTestCase) => void) { + for (const testCase of this.testCases) { + handler(testCase); + } + } + + private addTestCase(testCase: IComparerTestCase, addReversed: boolean) { + this.testCases.push(testCase); + if (addReversed) { + this.testCases.push({ + name: `${testCase.name} (reversed)`, + first: testCase.second, + second: testCase.first, + expected: testCase.expected, + }); + } + return this; + } +} diff --git a/tests/unit/application/Common/Array.spec.ts b/tests/unit/application/Common/Array.spec.ts new file mode 100644 index 00000000..d5609c0a --- /dev/null +++ b/tests/unit/application/Common/Array.spec.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest'; +import { scrambledEqual, sequenceEqual } from '@/application/Common/Array'; +import { ComparerTestScenario } from './Array.ComparerTestScenario'; + +describe('Array', () => { + describe('scrambledEqual', () => { + describe('returns as expected', () => { + // arrange + const scenario = new ComparerTestScenario() + .addSameItemsWithSameOrder(true) + .addSameItemsWithDifferentOrder(true) + .addDifferentItemsWithSameLength(false) + .addDifferentItemsWithDifferentLength(false); + // act + scenario.forEachCase((testCase) => { + it(testCase.name, () => { + const actual = scrambledEqual(testCase.first, testCase.second); + // assert + expect(actual).to.equal(testCase.expected); + }); + }); + }); + }); + describe('sequenceEqual', () => { + describe('returns as expected', () => { + // arrange + const scenario = new ComparerTestScenario() + .addSameItemsWithSameOrder(true) + .addSameItemsWithDifferentOrder(false) + .addDifferentItemsWithSameLength(false) + .addDifferentItemsWithDifferentLength(false); + // act + scenario.forEachCase((testCase) => { + it(testCase.name, () => { + const actual = sequenceEqual(testCase.first, testCase.second); + // assert + expect(actual).to.equal(testCase.expected); + }); + }); + }); + }); +}); diff --git a/tests/unit/application/Common/CustomError.spec.ts b/tests/unit/application/Common/CustomError.spec.ts new file mode 100644 index 00000000..dc7c8c6f --- /dev/null +++ b/tests/unit/application/Common/CustomError.spec.ts @@ -0,0 +1,174 @@ +import { + describe, it, afterEach, expect, +} from 'vitest'; +import { CustomError, PlatformErrorPrototypeManipulation } from '@/application/Common/CustomError'; + +describe('CustomError', () => { + afterEach(() => { + PlatformErrorPrototypeManipulation.getSetPrototypeOf = () => Object.setPrototypeOf; + PlatformErrorPrototypeManipulation.getCaptureStackTrace = () => Error.captureStackTrace; + }); + describe('sets members as expected', () => { + it('`name`', () => { + // arrange + const expectedName = CustomErrorConcrete.name; + // act + const sut = new CustomErrorConcrete(); + // assert + expect(sut.name).to.equal(expectedName); + }); + it('`message`', () => { + // arrange + const expectedMessage = 'expected message'; + // act + const sut = new CustomErrorConcrete(expectedMessage); + // assert + expect(sut.message).to.equal(expectedMessage); + }); + it('`cause`', () => { + // arrange + const expectedCause = new Error('expected cause'); + // act + const sut = new CustomErrorConcrete(undefined, { + cause: expectedCause, + }); + // assert + expect(sut.cause).to.equal(expectedCause); + }); + describe('`stack`', () => { + it('sets using `getCaptureStackTrace` if available', () => { + // arrange + const mockStackTrace = 'mocked stack trace'; + PlatformErrorPrototypeManipulation.getCaptureStackTrace = () => (error) => { + (error as Error).stack = mockStackTrace; + }; + // act + const sut = new CustomErrorConcrete(); + // assert + expect(sut.stack).to.equal(mockStackTrace); + }); + it('defined', () => { + // arrange + const customError = new CustomErrorConcrete(); + + // act + const { stack } = customError; + + // assert + expect(stack).to.not.equal(undefined); + }); + }); + }); + describe('retains correct prototypes', () => { + it('instance of `Error`', () => { + // arrange + const expected = Error; + // act + const sut = new CustomErrorConcrete(); + // assert + expect(sut).to.be.an.instanceof(expected); + }); + it('instance of `CustomErrorConcrete`', () => { + // arrange + const expected = CustomErrorConcrete; + // act + const sut = new CustomErrorConcrete(); + // assert + expect(sut).to.be.an.instanceof(expected); + }); + it('instance of `CustomError`', () => { + // arrange + const expected = CustomError; + // act + const sut = new CustomErrorConcrete(); + // assert + expect(sut).to.be.an.instanceof(expected); + }); + it('thrown error retains `CustomError` type', () => { + // arrange + const expected = CustomError; + let thrownError: unknown; + // act + try { + throw new CustomErrorConcrete('message'); + } catch (e) { + thrownError = e; + } + // assert + expect(thrownError).to.be.an.instanceof(expected); + }); + }); + describe('environment compatibility', () => { + describe('Object.setPrototypeOf', () => { + it('does not throw if unavailable', () => { + // arrange + PlatformErrorPrototypeManipulation.getSetPrototypeOf = () => undefined; + + // act + const act = () => new CustomErrorConcrete(); + + // assert + expect(act).to.not.throw(); + }); + it('calls if available', () => { + // arrange + let wasCalled = false; + const setPrototypeOf = () => { wasCalled = true; }; + PlatformErrorPrototypeManipulation.getSetPrototypeOf = () => setPrototypeOf; + + // act + // eslint-disable-next-line no-new + new CustomErrorConcrete(); + + // assert + expect(wasCalled).to.equal(true); + }); + }); + describe('Error.captureStackTrace', () => { + it('does not throw if unavailable', () => { + // arrange + PlatformErrorPrototypeManipulation.getCaptureStackTrace = () => undefined; + + // act + const act = () => new CustomErrorConcrete(); + + // assert + expect(act).to.not.throw(); + }); + it('calls if available', () => { + // arrange + let wasCalled = false; + const captureStackTrace = () => { wasCalled = true; }; + PlatformErrorPrototypeManipulation.getCaptureStackTrace = () => captureStackTrace; + + // act + // eslint-disable-next-line no-new + new CustomErrorConcrete(); + + // assert + expect(wasCalled).to.equal(true); + }); + }); + }); + describe('runtime behavior sanity checks', () => { + /* + * These tests are intended to verify the behavior of the JavaScript runtime or environment, + * rather than specific application logic. Typically, we avoid such tests because we + * trust the behavior of the underlying platform. However, they've been included here + * due to previous unexpected issues, specifically failures when trying to log + * `new Error().stack`. These issues arose because of factors like transpilation, + * source-mapping, and variances in JavaScript engine behaviors. + */ + it('`Error.stack` is defined', () => { + const error = new Error(); + + // act + const { stack } = error; + + // assert + expect(stack).to.not.equal(undefined); + }); + }); +}); + +class CustomErrorConcrete extends CustomError { } diff --git a/tests/unit/application/Common/Enum.spec.ts b/tests/unit/application/Common/Enum.spec.ts new file mode 100644 index 00000000..a79fbebf --- /dev/null +++ b/tests/unit/application/Common/Enum.spec.ts @@ -0,0 +1,112 @@ +import { describe, it, expect } from 'vitest'; +import { + getEnumNames, getEnumValues, createEnumParser, assertInRange, +} from '@/application/Common/Enum'; +import { scrambledEqual } from '@/application/Common/Array'; +import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; +import { EnumRangeTestRunner } from './EnumRangeTestRunner'; + +describe('Enum', () => { + describe('createEnumParser', () => { + enum ParsableEnum { Value1, value2 } + describe('parses as expected', () => { + // arrange + const testCases = [ + { + name: 'case insensitive', + value: 'vALuE1', + expected: ParsableEnum.Value1, + }, + { + name: 'exact match', + value: 'value2', + expected: ParsableEnum.value2, + }, + ]; + // act + for (const testCase of testCases) { + it(testCase.name, () => { + const parser = createEnumParser(ParsableEnum); + const actual = parser.parseEnum(testCase.value, 'non-important'); + // assert + expect(actual).to.equal(testCase.expected); + }); + } + }); + describe('throws as expected', () => { + // arrange + const enumName = 'ParsableEnum'; + const testScenarios: ReadonlyArray<{ + readonly name: string; + readonly value: string; + readonly expectedError: string; + }> = [ + ...getAbsentStringTestCases({ + excludeNull: true, + excludeUndefined: true, + }).map((test) => ({ + name: test.valueName, + value: test.absentValue, + expectedError: `missing ${enumName}`, + })), + { + name: 'out of range', + value: 'value3', + expectedError: `unknown ${enumName}: "value3"`, + }, + { + name: 'out of range', + value: 'value3', + expectedError: `unknown ${enumName}: "value3"`, + }, + { + name: 'unexpected type', + value: 55 as never, + expectedError: `unexpected type of ${enumName}: "number"`, + }, + ]; + // act + for (const testCase of testScenarios) { + it(testCase.name, () => { + const parser = createEnumParser(ParsableEnum); + const act = () => parser.parseEnum(testCase.value, enumName); + // assert + expect(act).to.throw(testCase.expectedError); + }); + } + }); + }); + describe('getEnumNames', () => { + it('parses as expected', () => { + // arrange + enum TestEnum { TestValue1, testValue2, testvalue3, TESTVALUE4 } + const expected = ['TestValue1', 'testValue2', 'testvalue3', 'TESTVALUE4']; + // act + const actual = getEnumNames(TestEnum); + // assert + expect(scrambledEqual(expected, actual)); + }); + }); + describe('getEnumValues', () => { + it('parses as expected', () => { + // arrange + enum TestEnum { Red, Green, Blue } + const expected = [TestEnum.Red, TestEnum.Green, TestEnum.Blue]; + // act + const actual = getEnumValues(TestEnum); + // assert + expect(scrambledEqual(expected, actual)); + }); + }); + describe('assertInRange', () => { + // arrange + enum TestEnum { Red, Green, Blue } + const validValue = TestEnum.Red; + // act + const act = (value: TestEnum) => assertInRange(value, TestEnum); + // assert + new EnumRangeTestRunner(act) + .testOutOfRangeThrows() + .testValidValueDoesNotThrow(validValue); + }); +}); diff --git a/tests/unit/application/Common/EnumRangeTestRunner.ts b/tests/unit/application/Common/EnumRangeTestRunner.ts new file mode 100644 index 00000000..a26caafd --- /dev/null +++ b/tests/unit/application/Common/EnumRangeTestRunner.ts @@ -0,0 +1,46 @@ +import { it, expect } from 'vitest'; +import type { EnumType } from '@/application/Common/Enum'; + +export class EnumRangeTestRunner { + constructor(private readonly runner: (value: TEnumValue) => void) { + } + + public testOutOfRangeThrows(errorMessageBuilder?: (outOfRangeValue: TEnumValue) => string) { + it('throws when value is out of range', () => { + // arrange + const value = Number.MAX_SAFE_INTEGER as TEnumValue; + const expectedError = errorMessageBuilder + ? errorMessageBuilder(value) + : `enum value "${value}" is out of range`; + // act + const act = () => this.runner(value); + // assert + expect(act).to.throw(expectedError); + }); + return this; + } + + public testInvalidValueThrows(invalidValue: TEnumValue, expectedError: string) { + it(`throws: \`${expectedError}\``, () => { + // arrange + const value = invalidValue; + // act + const act = () => this.runner(value); + // assert + expect(act).to.throw(expectedError); + }); + return this; + } + + public testValidValueDoesNotThrow(validValue: TEnumValue) { + it('does not throw with valid value', () => { + // arrange + const value = validValue; + // act + const act = () => this.runner(value); + // assert + expect(act).to.not.throw(); + }); + return this; + } +} diff --git a/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactory.spec.ts b/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactory.spec.ts new file mode 100644 index 00000000..7d9b7d3d --- /dev/null +++ b/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactory.spec.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest'; +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory'; +import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner'; +import { ScriptingLanguageFactoryTestRunner } from './ScriptingLanguageFactoryTestRunner'; + +class ScriptingLanguageConcrete extends ScriptingLanguageFactory { + public registerGetter(language: ScriptingLanguage, getter: () => number) { + super.registerGetter(language, getter); + } +} + +describe('ScriptingLanguageFactory', () => { + describe('registerGetter', () => { + describe('validates language', () => { + // arrange + const validValue = ScriptingLanguage.batchfile; + const getter = () => 1; + const sut = new ScriptingLanguageConcrete(); + // act + const act = (language: ScriptingLanguage) => sut.registerGetter(language, getter); + // assert + new EnumRangeTestRunner(act) + .testOutOfRangeThrows() + .testValidValueDoesNotThrow(validValue); + }); + it('throw when language is already registered', () => { + // arrange + const language = ScriptingLanguage.batchfile; + const expectedError = `${ScriptingLanguage[language]} is already registered`; + const getter = () => 1; + const sut = new ScriptingLanguageConcrete(); + // act + sut.registerGetter(language, getter); + const reRegister = () => sut.registerGetter(language, getter); + // assert + expect(reRegister).to.throw(expectedError); + }); + }); + describe('create', () => { + // arrange + const sut = new ScriptingLanguageConcrete(); + const runner = new ScriptingLanguageFactoryTestRunner(); + // act + sut.registerGetter(ScriptingLanguage.batchfile, () => ScriptingLanguage.batchfile); + sut.registerGetter(ScriptingLanguage.shellscript, () => ScriptingLanguage.shellscript); + // assert + runner + .expectValue(ScriptingLanguage.shellscript, ScriptingLanguage.shellscript) + .expectValue(ScriptingLanguage.batchfile, ScriptingLanguage.batchfile) + .testCreateMethod(sut); + }); +}); diff --git a/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner.ts b/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner.ts new file mode 100644 index 00000000..ea930e78 --- /dev/null +++ b/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from 'vitest'; +import type { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory'; +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner'; + +type Constructible = new (...args: unknown[]) => T; + +export class ScriptingLanguageFactoryTestRunner { + private expectedLanguageTypes = new Map>(); + + private expectedValues = new Map(); + + public expectInstance(language: ScriptingLanguage, resultType: Constructible) { + this.expectedLanguageTypes.set(language, resultType); + return this; + } + + public expectValue(language: ScriptingLanguage, resultType: T) { + this.expectedValues.set(language, resultType); + return this; + } + + public testCreateMethod(sut: IScriptingLanguageFactory) { + testLanguageValidation(sut); + if (this.expectedLanguageTypes.size) { + testExpectedInstanceTypes(sut, this.expectedLanguageTypes); + } + if (this.expectedValues.size) { + testExpectedValues(sut, this.expectedValues); + } + } +} + +function testExpectedInstanceTypes( + sut: IScriptingLanguageFactory, + expectedTypes: Map>, +) { + if (!expectedTypes.size) { + throw new Error('No expected types provided.'); + } + describe('`create` creates expected instances', () => { + // arrange + for (const language of expectedTypes.keys()) { + it(ScriptingLanguage[language], () => { + // act + const expected = expectedTypes.get(language); + const result = sut.create(language); + // assert + expect(result).to.be.instanceOf(expected, `Actual was: ${result}`); + }); + } + }); +} + +function testExpectedValues( + sut: IScriptingLanguageFactory, + expectedValues: Map, +) { + if (!expectedValues.size) { + throw new Error('No expected values provided.'); + } + describe('`create` creates expected values', () => { + // arrange + for (const language of expectedValues.keys()) { + it(ScriptingLanguage[language], () => { + // act + const expected = expectedValues.get(language); + const result = sut.create(language); + // assert + expect(result).to.equal(expected); + }); + } + }); +} + +function testLanguageValidation(sut: IScriptingLanguageFactory) { + describe('`create` validates language selection', () => { + // arrange + const validValue = ScriptingLanguage.batchfile; + // act + const act = (value: ScriptingLanguage) => sut.create(value); + // assert + new EnumRangeTestRunner(act) + .testOutOfRangeThrows() + .testValidValueDoesNotThrow(validValue); + }); +} diff --git a/tests/unit/application/Common/Shuffle.spec.ts b/tests/unit/application/Common/Shuffle.spec.ts new file mode 100644 index 00000000..ebbb0aa9 --- /dev/null +++ b/tests/unit/application/Common/Shuffle.spec.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { shuffle } from '@/application/Common/Shuffle'; + +describe('Shuffle', () => { + describe('shuffle', () => { + it('returns a new array', () => { + // arrange + const inputArray = ['a', 'b', 'c', 'd']; + // act + const result = shuffle(inputArray); + // assert + expect(result).not.to.equal(inputArray); + }); + + it('returns an array of the same length', () => { + // arrange + const inputArray = ['a', 'b', 'c', 'd']; + // act + const result = shuffle(inputArray); + // assert + expect(result.length).toBe(inputArray.length); + }); + + it('contains the same elements', () => { + // arrange + const inputArray = ['a', 'b', 'c', 'd']; + // act + const result = shuffle(inputArray); + // assert + expect(result).to.have.members(inputArray); + }); + + it('does not modify the input array', () => { + // arrange + const inputArray = ['a', 'b', 'c', 'd']; + const inputArrayCopy = [...inputArray]; + // act + shuffle(inputArray); + // assert + expect(inputArray).to.deep.equal(inputArrayCopy); + }); + + it('handles an empty array correctly', () => { + // arrange + const inputArray: string[] = []; + // act + const result = shuffle(inputArray); + // assert + expect(result).have.lengthOf(0); + }); + }); +}); diff --git a/tests/unit/application/Common/Text/FilterEmptyStrings.spec.ts b/tests/unit/application/Common/Text/FilterEmptyStrings.spec.ts new file mode 100644 index 00000000..06c77e28 --- /dev/null +++ b/tests/unit/application/Common/Text/FilterEmptyStrings.spec.ts @@ -0,0 +1,99 @@ +import { describe, it, expect } from 'vitest'; +import { filterEmptyStrings, type OptionalString } from '@/application/Common/Text/FilterEmptyStrings'; +import { IsArrayStub } from '@tests/unit/shared/Stubs/IsArrayStub'; +import type { isArray } from '@/TypeHelpers'; + +describe('filterEmptyStrings', () => { + describe('filtering behavior', () => { + // arrange + const testScenarios: readonly { + readonly description: string; + readonly texts: readonly OptionalString[]; + readonly expected: readonly string[]; + }[] = [ + { + description: 'filters out non-string entries', + texts: ['Hello', '', 'World', null, 'Test', undefined], + expected: ['Hello', 'World', 'Test'], + }, + { + description: 'returns empty array for no valid strings', + texts: [null, undefined, ''], + expected: [], + }, + { + description: 'preserves all valid strings', + texts: ['Hello', 'World', 'Test'], + expected: ['Hello', 'World', 'Test'], + }, + ]; + testScenarios.forEach(({ + description, texts, expected, + }) => { + it(description, () => { + const context = new TestContext() + .withTexts(texts); + // act + const result = context.filterEmptyStrings(); + // assert + expect(result).to.deep.equal(expected); + }); + }); + }); + + describe('error handling', () => { + it('throws for non-array input', () => { + // arrange + const nonArrayInput = 'Hello'; + const isArray = new IsArrayStub() + .withPredeterminedResult(false) + .get(); + const expectedErrorMessage = `Invalid input: Expected an array, but received type ${typeof nonArrayInput}.`; + const context = new TestContext() + .withTexts(nonArrayInput as unknown as OptionalString[]) + .withIsArrayType(isArray); + // act + const act = () => context.filterEmptyStrings(); + // assert + expect(act).toThrow(expectedErrorMessage); + }); + + it('throws for invalid item types in array', () => { + // arrange + const invalidInput: unknown[] = ['Hello', 42, 'World']; // Number is invalid + const expectedErrorMessage = 'Invalid array items: Expected items as string, undefined, or null. Received invalid types: number.'; + const context = new TestContext() + .withTexts(invalidInput as OptionalString[]); + // act + const act = () => context.filterEmptyStrings(); + // assert + expect(act).to.throw(expectedErrorMessage); + }); + }); +}); + +class TestContext { + private texts: readonly OptionalString[] = [ + `[${TestContext.name}] text to stay after filtering`, + ]; + + private isArrayType: typeof isArray = new IsArrayStub() + .get(); + + public withTexts(texts: readonly OptionalString[]): this { + this.texts = texts; + return this; + } + + public withIsArrayType(isArrayType: typeof isArray): this { + this.isArrayType = isArrayType; + return this; + } + + public filterEmptyStrings(): ReturnType { + return filterEmptyStrings( + this.texts, + this.isArrayType, + ); + } +} diff --git a/tests/unit/application/Common/Text/IndentText.spec.ts b/tests/unit/application/Common/Text/IndentText.spec.ts new file mode 100644 index 00000000..c71a9e36 --- /dev/null +++ b/tests/unit/application/Common/Text/IndentText.spec.ts @@ -0,0 +1,130 @@ +import { describe, it, expect } from 'vitest'; +import { indentText } from '@/application/Common/Text/IndentText'; +import { IsStringStub } from '@tests/unit/shared/Stubs/IsStringStub'; +import type { isString } from '@/TypeHelpers'; + +type IndentLevel = Parameters['1']; + +const TestLineSeparator = '[TEST-LINE-SEPARATOR]'; + +describe('indentText', () => { + describe('text indentation', () => { + const testScenarios: readonly { + readonly description: string; + readonly text: string; + readonly indentLevel: IndentLevel; + readonly expected: string; + }[] = [ + { + description: 'indents multiple lines with single tab', + text: createMultilineTestInput('Hello', 'World', 'Test'), + indentLevel: 1, + expected: '\tHello\n\tWorld\n\tTest', + }, + { + description: 'indents multiple lines with two tabs', + text: createMultilineTestInput('Hello', 'World', 'Test'), + indentLevel: 2, + expected: '\t\tHello\n\t\tWorld\n\t\tTest', + }, + { + description: 'indents single line with one tab', + text: 'Hello World', + indentLevel: 1, + expected: '\tHello World', + }, + { + description: 'preserves empty string without indentation', + text: '', + indentLevel: 1, + expected: '', + }, + { + description: 'defaults to one tab when indent level is unspecified', + text: createMultilineTestInput('Hello', 'World'), + indentLevel: undefined, + expected: '\tHello\n\tWorld', + }, + ]; + testScenarios.forEach(({ + description, text, indentLevel, expected, + }) => { + it(description, () => { + const context = new TextContext() + .withText(text) + .withIndentLevel(indentLevel); + // act + const actualText = context.indentText(); + // assert + expect(actualText).to.equal(expected); + }); + }); + }); + + describe('error handling', () => { + it('throws for non-string input', () => { + // arrange + const invalidInput = 42; + const expectedErrorMessage = `Indentation error: The input must be a string. Received type: ${typeof invalidInput}.`; + const isString = new IsStringStub() + .withPredeterminedResult(false) + .get(); + const context = new TextContext() + .withText(invalidInput as unknown as string /* bypass compiler checks */) + .withIsStringType(isString); + // act + const act = () => context.indentText(); + // assert + expect(act).toThrow(expectedErrorMessage); + }); + it('throws for indentation level below one', () => { + // arrange + const indentLevel = 0; + const expectedErrorMessage = `Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`; + const context = new TextContext() + .withIndentLevel(indentLevel); + // act + const act = () => context.indentText(); + // assert + expect(act).toThrow(expectedErrorMessage); + }); + }); +}); + +function createMultilineTestInput(...lines: readonly string[]): string { + return lines.join(TestLineSeparator); +} + +class TextContext { + private text = `[${TextContext.name}] text to indent`; + + private indentLevel: IndentLevel = undefined; + + private isStringType: typeof isString = new IsStringStub().get(); + + public withText(text: string): this { + this.text = text; + return this; + } + + public withIndentLevel(indentLevel: IndentLevel): this { + this.indentLevel = indentLevel; + return this; + } + + public withIsStringType(isStringType: typeof isString): this { + this.isStringType = isStringType; + return this; + } + + public indentText(): ReturnType { + return indentText( + this.text, + this.indentLevel, + { + splitIntoLines: (text) => text.split(TestLineSeparator), + isStringType: this.isStringType, + }, + ); + } +} diff --git a/tests/unit/application/Common/Text/SplitTextIntoLines.spec.ts b/tests/unit/application/Common/Text/SplitTextIntoLines.spec.ts new file mode 100644 index 00000000..ae51c533 --- /dev/null +++ b/tests/unit/application/Common/Text/SplitTextIntoLines.spec.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from 'vitest'; +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; +import type { isString } from '@/TypeHelpers'; +import { IsStringStub } from '@tests/unit/shared/Stubs/IsStringStub'; + +describe('splitTextIntoLines', () => { + describe('splits correctly', () => { + // arrange + const testScenarios: readonly { + readonly description: string; + readonly text: string; + readonly expectedLines: readonly string[]; + } [] = [ + { + description: 'handles Unix-like line separator', + text: 'Hello\nWorld\nTest', + expectedLines: ['Hello', 'World', 'Test'], + }, + { + description: 'handles Windows line separator', + text: 'Hello\r\nWorld\r\nTest', + expectedLines: ['Hello', 'World', 'Test'], + }, + { + description: 'handles mixed indentation (both Unix-like and Windows)', + text: 'Hello\r\nWorld\nTest', + expectedLines: ['Hello', 'World', 'Test'], + }, + { + description: 'returns an array with one element when no new lines', + text: 'Hello World', + expectedLines: ['Hello World'], + }, + { + description: 'preserves empty lines between text lines', + text: 'Hello\n\nWorld\n\n\nTest\n', + expectedLines: ['Hello', '', 'World', '', '', 'Test', ''], + }, + { + description: 'handles empty strings', + text: '', + expectedLines: [''], + }, + ]; + testScenarios.forEach(({ + description, text, expectedLines, + }) => { + it(description, () => { + const testContext = new TestContext() + .withText(text); + // act + const result = testContext.splitText(); + // assert + expect(result).to.deep.equal(expectedLines); + }); + }); + }); + it('checks for string type', () => { + // arrange + const invalidInput = 42; + const errorMessage = `Line splitting error: Expected a string but received type '${typeof invalidInput}'.`; + const isString = new IsStringStub() + .withPredeterminedResult(false) + .get(); + // act + const act = () => new TestContext() + .withText(invalidInput as unknown as string) + .withIsStringType(isString) + .splitText(); + // assert + expect(act).to.throw(errorMessage); + }); +}); + +class TestContext { + private isStringType: typeof isString = new IsStringStub().get(); + + private text: string = `[${TestContext.name}] text value`; + + public withText(text: string): this { + this.text = text; + return this; + } + + public withIsStringType(isStringType: typeof isString): this { + this.isStringType = isStringType; + return this; + } + + public splitText(): ReturnType { + return splitTextIntoLines( + this.text, + this.isStringType, + ); + } +} diff --git a/tests/unit/application/Common/Timing/BatchedDebounce.spec.ts b/tests/unit/application/Common/Timing/BatchedDebounce.spec.ts new file mode 100644 index 00000000..8943b06b --- /dev/null +++ b/tests/unit/application/Common/Timing/BatchedDebounce.spec.ts @@ -0,0 +1,144 @@ +import { describe, it, expect } from 'vitest'; +import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce'; +import { TimerStub } from '@tests/unit/shared/Stubs/TimerStub'; + +describe('batchedDebounce', () => { + describe('immediate invocation', () => { + it('does not call the callback immediately on the first call', () => { + // arrange + const { calledBatches, callback } = createObservableCallback(); + const callArg = 'first'; + const debounceFunc = batchedDebounce(callback, 100, new TimerStub()); + + // act + debounceFunc(callArg); + + // assert + expect(calledBatches).to.have.lengthOf(0); + }); + }); + + describe('debounce timing', () => { + it('executes the callback after the debounce period', () => { + // arrange + const { calledBatches, callback } = createObservableCallback(); + const expectedArg = 'first'; + const debouncePeriodInMs = 100; + const timer = new TimerStub(); + const debounceFunc = batchedDebounce(callback, debouncePeriodInMs, timer); + + // act + debounceFunc(expectedArg); + timer.tickNext(debouncePeriodInMs); + + // assert + expect(calledBatches).to.have.lengthOf(1); + expect(calledBatches).to.deep.include([expectedArg]); + }); + it('prevents callback invocation within the debounce period', () => { + // arrange + const { calledBatches, callback } = createObservableCallback(); + const debouncePeriodInMs = 100; + const timer = new TimerStub(); + const debounceFunc = batchedDebounce(callback, debouncePeriodInMs, timer); + + // act + debounceFunc('first'); + timer.tickNext(debouncePeriodInMs / 4); + debounceFunc('second'); + timer.tickNext(debouncePeriodInMs / 4); + debounceFunc('third'); + timer.tickNext(debouncePeriodInMs / 4); + + // assert + expect(calledBatches).to.have.lengthOf(0); + }); + it('resets debounce timer on subsequent calls', () => { + // arrange + const timer = new TimerStub(); + const { calledBatches, callback } = createObservableCallback(); + const debouncePeriodInMs = 100; + const debounceFunc = batchedDebounce(callback, debouncePeriodInMs, timer); + + // act + debounceFunc('first'); + timer.tickNext(debouncePeriodInMs * 0.9); + debounceFunc('second'); + timer.tickNext(debouncePeriodInMs * 0.9); + debounceFunc('third'); + timer.tickNext(debouncePeriodInMs * 0.9); + + // assert + expect(calledBatches).to.have.lengthOf(0); + }); + it('does not call the callback again if no new calls are made after the debounce period', () => { + // arrange + const { calledBatches, callback } = createObservableCallback(); + const debouncePeriodInMs = 100; + const timer = new TimerStub(); + const debounceFunc = batchedDebounce(callback, debouncePeriodInMs, timer); + + // act + debounceFunc('first'); + timer.tickNext(debouncePeriodInMs); + timer.tickNext(debouncePeriodInMs); + timer.tickNext(debouncePeriodInMs); + timer.tickNext(debouncePeriodInMs); + + // assert + expect(calledBatches).to.have.lengthOf(1); + }); + }); + + describe('batching calls', () => { + it('batches multiple calls within the debounce period', () => { + // arrange + const { calledBatches, callback } = createObservableCallback(); + const firstCallArg = 'first'; + const secondCallArg = 'second'; + const debouncePeriodInMs = 100; + const timer = new TimerStub(); + const debounceFunc = batchedDebounce(callback, debouncePeriodInMs, timer); + + // act + debounceFunc(firstCallArg); + debounceFunc(secondCallArg); + timer.tickNext(debouncePeriodInMs); + + // assert + expect(calledBatches).to.have.lengthOf(1); + expect(calledBatches).to.deep.include([firstCallArg, secondCallArg]); + }); + it('handles multiple separate batches correctly', () => { + // arrange + const { calledBatches, callback } = createObservableCallback(); + const debouncePeriodInMs = 100; + const firstBatchArg = 'first'; + const secondBatchArg = 'second'; + const timer = new TimerStub(); + const debounceFunc = batchedDebounce(callback, debouncePeriodInMs, timer); + + // act + debounceFunc(firstBatchArg); + timer.tickNext(debouncePeriodInMs); + debounceFunc(secondBatchArg); + timer.tickNext(debouncePeriodInMs); + + // assert + expect(calledBatches).to.have.lengthOf(2); + expect(calledBatches[0]).to.deep.equal([firstBatchArg]); + expect(calledBatches[1]).to.deep.equal([secondBatchArg]); + }); + }); +}); + +function createObservableCallback() { + const calledBatches = new Array(); + const callback = (batches: readonly string[]): void => { + calledBatches.push(batches); + }; + return { + calledBatches, + callback, + }; +} diff --git a/tests/unit/application/Common/Timing/PlatformTimer.spec.ts b/tests/unit/application/Common/Timing/PlatformTimer.spec.ts new file mode 100644 index 00000000..61e80c13 --- /dev/null +++ b/tests/unit/application/Common/Timing/PlatformTimer.spec.ts @@ -0,0 +1,78 @@ +import { + describe, it, expect, beforeEach, + afterEach, +} from 'vitest'; +import { PlatformTimer } from '@/application/Common/Timing/PlatformTimer'; + +describe('PlatformTimer', () => { + let originalSetTimeout: typeof global.setTimeout; + let originalClearTimeout: typeof global.clearTimeout; + let originalDateNow: typeof global.Date.now; + + beforeEach(() => { + originalSetTimeout = global.setTimeout; + originalClearTimeout = global.clearTimeout; + originalDateNow = Date.now; + Date.now = () => originalDateNow(); + }); + + afterEach(() => { + global.setTimeout = originalSetTimeout; + global.clearTimeout = originalClearTimeout; + Date.now = originalDateNow; + }); + + describe('setTimeout', () => { + it('calls the global setTimeout with the provided delay', () => { + // arrange + const expectedDelay = 55; + let actualDelay: number | undefined; + global.setTimeout = ((_, delay) => { + actualDelay = delay; + }) as typeof global.setTimeout; + // act + PlatformTimer.setTimeout(() => { /* NOOP */ }, expectedDelay); + // assert + expect(actualDelay).to.equal(expectedDelay); + }); + it('calls the global setTimeout with the provided callback', () => { + // arrange + const expectedCallback = () => { /* NOOP */ }; + let actualCallback: typeof expectedCallback | undefined; + global.setTimeout = ((callback) => { + actualCallback = callback; + }) as typeof global.setTimeout; + // act + PlatformTimer.setTimeout(expectedCallback, 33); + // assert + expect(actualCallback).to.equal(expectedCallback); + }); + }); + + describe('clearTimeout', () => { + it('should clear timeout', () => { + // arrange + let actualTimer: ReturnType | undefined; + global.clearTimeout = ((timer) => { + actualTimer = timer; + }) as typeof global.clearTimeout; + const expectedTimer = PlatformTimer.setTimeout(() => { /* NOOP */ }, 1); + // act + PlatformTimer.clearTimeout(expectedTimer); + // assert + expect(actualTimer).to.equal(expectedTimer); + }); + }); + + describe('dateNow', () => { + it('should get current date', () => { + // arrange + const expected = Date.now(); + Date.now = () => expected; + // act + const actual = PlatformTimer.dateNow(); + // assert + expect(expected).to.equal(actual); + }); + }); +}); diff --git a/tests/unit/application/Common/Timing/Throttle.spec.ts b/tests/unit/application/Common/Timing/Throttle.spec.ts new file mode 100644 index 00000000..9ce812e9 --- /dev/null +++ b/tests/unit/application/Common/Timing/Throttle.spec.ts @@ -0,0 +1,325 @@ +import { describe, it, expect } from 'vitest'; +import { TimerStub } from '@tests/unit/shared/Stubs/TimerStub'; +import { throttle, type ThrottleFunction, type ThrottleOptions } from '@/application/Common/Timing/Throttle'; +import type { Timer } from '@/application/Common/Timing/Timer'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; + +describe('throttle', () => { + describe('parameter validation', () => { + describe('throws for invalid waitInMs', () => { + const testCases: readonly { + readonly description: string; + readonly value: number; + readonly expectedError: string; + }[] = [ + { + description: 'given zero', + value: 0, + expectedError: 'missing delay', + }, + { + description: 'given negative', + value: -2, + expectedError: 'negative delay', + }, + ]; + testCases.forEach(( + { description, expectedError, value: waitInMs }, + ) => { + it(`"${description}" throws "${expectedError}"`, () => { + // arrange + const context = new TestContext() + .withWaitInMs(waitInMs); + // act + const act = () => context.throttle(); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); + }); + it('executes the leading callback immediately', () => { + // arrange + const timer = new TimerStub(); + let totalRuns = 0; + const callback = () => totalRuns++; + const throttleFunc = new TestContext() + .withTimer(timer) + .withCallback(callback) + .throttle(); + + // act + throttleFunc(); + + // assert + expect(totalRuns).to.equal(1); + }); + it('executes the leading callback with initial arguments', () => { + // arrange + const expectedArguments = [1, 2, 3]; + const timer = new TimerStub(); + let lastArgs: readonly number[] | null = null; + const callback = (...args: readonly number[]) => { lastArgs = args; }; + const waitInMs = 500; + const throttleFunc = new TestContext() + .withWaitInMs(waitInMs) + .withTimer(timer) + .withCallback(callback) + .throttle(); + + // act + throttleFunc(...expectedArguments); + timer.tickNext(waitInMs / 3); + throttleFunc(4, 5, 6); + timer.tickNext(waitInMs / 3); + throttleFunc(7, 8, 9); + + // assert + expect(lastArgs).to.deep.equal(expectedArguments); + }); + it('executes the trailing callback with final arguments', () => { + // arrange + const expectedArguments = [1, 2, 3]; + const timer = new TimerStub(); + let lastArgs: readonly number[] | null = null; + const callback = (...args: readonly number[]) => { lastArgs = args; }; + const waitInMs = 500; + const throttleFunc = new TestContext() + .withWaitInMs(waitInMs) + .withTimer(timer) + .withCallback(callback) + .throttle(); + + // act + throttleFunc(1, 2, 3); + timer.tickNext(100); + throttleFunc(4, 5, 6); + timer.tickNext(100); + throttleFunc(lastArgs); + + // assert + expect(lastArgs).to.deep.equal(expectedArguments); + }); + it('executes the callback after the delay', () => { + // arrange + const timer = new TimerStub(); + let totalRuns = 0; + const callback = () => totalRuns++; + const waitInMs = 500; + const throttleFunc = new TestContext() + .withWaitInMs(waitInMs) + .withTimer(timer) + .withCallback(callback) + .throttle(); + // act + throttleFunc(); + totalRuns--; // So we don't count the initial run + throttleFunc(); + timer.tickNext(waitInMs); + // assert + expect(totalRuns).to.equal(1); + }); + it('limits calls to at most once per period', () => { + // arrange + const totalExpectedCalls = 2; // leading and trailing only + const timer = new TimerStub(); + let totalRuns = 0; + const callback = () => totalRuns++; + const waitInMs = 200; + const totalCalls = 10; + const throttleFunc = new TestContext() + .withWaitInMs(waitInMs) + .withTimer(timer) + .withCallback(callback) + .throttle(); + // act + for (let currentCall = 0; currentCall < totalCalls; currentCall++) { + const currentTime = (waitInMs / totalCalls) * currentCall; + timer.setCurrentTime(currentTime); + throttleFunc(); + } + timer.tickNext(waitInMs); + // assert + expect(totalRuns).to.equal(totalExpectedCalls); + }); + it('executes the callback after each complete delay period', () => { + // arrange + const timer = new TimerStub(); + let totalRuns = 0; + const callback = () => totalRuns++; + const waitInMs = 500; + const expectedTotalRuns = 10; + const throttleFunc = new TestContext() + .withWaitInMs(waitInMs) + .withTimer(timer) + .withCallback(callback) + .throttle(); + // act + Array.from({ length: expectedTotalRuns }).forEach(() => { + throttleFunc(); + timer.tickNext(waitInMs); + }); + // assert + expect(totalRuns).to.equal(expectedTotalRuns); + }); + describe('leading call exclusion', () => { + it('does not execute the callback immediately on the first call', () => { + // arrange + const timer = new TimerStub(); + let totalRuns = 0; + const callback = () => totalRuns++; + const throttleFunc = new TestContext() + .withTimer(timer) + .withCallback(callback) + .withExcludeLeadingCall(true) + .throttle(); + // act + throttleFunc(); + // assert + expect(totalRuns).to.equal(0); + }); + it('executes the initial call after the initial wait time', () => { + // arrange + const timer = new TimerStub(); + let totalRuns = 0; + const waitInMs = 200; + const callback = () => totalRuns++; + const throttleFunc = new TestContext() + .withTimer(timer) + .withCallback(callback) + .withExcludeLeadingCall(true) + .withWaitInMs(waitInMs) + .throttle(); + // act + throttleFunc(); + timer.tickNext(waitInMs); + // assert + expect(totalRuns).to.equal(1); + }); + it('executes two calls after two wait periods', () => { + // arrange + const expectedTotalRuns = 2; + const calledArgs = new Array(); + const timer = new TimerStub(); + const waitInMs = 300; + let totalRuns = 0; + const callback = (message: string) => { + totalRuns++; + calledArgs.push(message); + }; + const throttleFunc = new TestContext() + .withTimer(timer) + .withCallback(callback) + .withWaitInMs(waitInMs) + .withExcludeLeadingCall(true) + .throttle(); + // act + Array.from({ length: expectedTotalRuns }).forEach((_, index) => { + throttleFunc(`Call ${index} (zero-based, where initial call is 0)`); + timer.tickNext(waitInMs); + }); + // assert + expect(totalRuns).to.equal(expectedTotalRuns, formatAssertionMessage([ + `Expected total runs to equal ${expectedTotalRuns}, but got ${totalRuns}.`, + 'Detailed call information:', + ...calledArgs.map((message, index) => `${index + 1}) ${message}`), + ])); + }); + it('only executes once when multiple calls are made during the initial wait period', () => { + // arrange + const timer = new TimerStub(); + let totalRuns = 0; + const callback = () => { totalRuns++; }; + const waitInMs = 300; + const throttleFunc = new TestContext() + .withTimer(timer) + .withWaitInMs(waitInMs) + .withCallback(callback) + .withExcludeLeadingCall(true) + .throttle(); + // act + throttleFunc(); + timer.tickNext(waitInMs / 3); + throttleFunc(); + timer.tickNext(waitInMs / 3); + throttleFunc(); + timer.tickNext(waitInMs / 3); + // assert + expect(totalRuns).to.equal(1); + }); + it('executes the last provided arguments only after the wait period expires', () => { + // arrange + const expectedLastArg = 'trailing call'; + const timer = new TimerStub(); + let actualLastArg: string | null = null; + const callback = (arg: string) => { + actualLastArg = arg; + }; + const waitInMs = 300; + const throttleFunc = new TestContext() + .withTimer(timer) + .withWaitInMs(waitInMs) + .withCallback(callback) + .withExcludeLeadingCall(true) + .throttle(); + // act + throttleFunc('leading call'); + timer.tickNext(waitInMs / 3); + throttleFunc('call in the middle'); + timer.tickNext(waitInMs / 3); + throttleFunc(expectedLastArg); + timer.tickNext(waitInMs / 3); + // assert + expect(actualLastArg).to.equal(expectedLastArg); + }); + }); +}); + +type CallbackType = Parameters[0]; + +class TestContext { + private options: Partial | undefined = { + timer: new TimerStub(), + }; + + private waitInMs: number = 500; + + private callback: CallbackType = () => { /* NO OP */ }; + + public withTimer(timer: Timer): this { + return this.withOptions({ + ...(this.options ?? {}), + timer, + }); + } + + public withWaitInMs(waitInMs: number): this { + this.waitInMs = waitInMs; + return this; + } + + public withCallback(callback: CallbackType): this { + this.callback = callback; + return this; + } + + public withExcludeLeadingCall(excludeLeadingCall: boolean): this { + return this.withOptions({ + ...(this.options ?? {}), + excludeLeadingCall, + }); + } + + public withOptions(options: Partial | undefined): this { + this.options = options; + return this; + } + + public throttle(): ReturnType { + return throttle( + this.callback, + this.waitInMs, + this.options, + ); + } +} diff --git a/tests/unit/application/Context/ApplicationContext.spec.ts b/tests/unit/application/Context/ApplicationContext.spec.ts new file mode 100644 index 00000000..07264bc4 --- /dev/null +++ b/tests/unit/application/Context/ApplicationContext.spec.ts @@ -0,0 +1,261 @@ +import { describe, it, expect } from 'vitest'; +import { ApplicationContext } from '@/application/Context/ApplicationContext'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import type { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import type { IApplicationContext, IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext'; +import type { IApplication } from '@/domain/IApplication'; +import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; + +describe('ApplicationContext', () => { + describe('changeContext', () => { + describe('when initial os is changed to different one', () => { + it('collection is changed as expected', () => { + // arrange + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.Windows, OperatingSystem.macOS); + const expectedCollection = testContext.app.getCollection(OperatingSystem.macOS); + // act + const sut = testContext + .withInitialOs(OperatingSystem.Windows) + .construct(); + sut.changeContext(OperatingSystem.macOS); + // assert + expect(sut.state.collection).to.equal(expectedCollection); + }); + it('currentOs is changed as expected', () => { + // arrange + const expectedOs = OperatingSystem.macOS; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.Windows, expectedOs); + // act + const sut = testContext + .withInitialOs(OperatingSystem.Windows) + .construct(); + sut.changeContext(expectedOs); + // assert + expect(sut.state.os).to.equal(expectedOs); + }); + it('new state is empty', () => { + // arrange + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.Windows, OperatingSystem.macOS); + // act + const sut = testContext + .withInitialOs(OperatingSystem.Windows) + .construct(); + sut.state.filter.applyFilter('filtered'); + sut.changeContext(OperatingSystem.macOS); + // assert + expectEmptyState(sut.state); + }); + it('throws when OS is unknown to application', () => { + // arrange + const expectedError = 'expected error from application'; + const applicationStub = new ApplicationStub(); + const sut = new ObservableApplicationContextFactory() + .withApp(applicationStub) + .construct(); + // act + applicationStub.getCollection = () => { throw new Error(expectedError); }; + const act = () => sut.changeContext(OperatingSystem.Android); + // assert + expect(act).to.throw(expectedError); + }); + }); + it('remembers old state when changed backed to same os', () => { + // arrange + const os = OperatingSystem.Windows; + const changedOs = OperatingSystem.macOS; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(os, changedOs); + const expectedFilter = 'first-state'; + // act + const sut = testContext + .withInitialOs(os) + .construct(); + const firstState = sut.state; + firstState.filter.applyFilter(expectedFilter); + sut.changeContext(os); + sut.changeContext(changedOs); + sut.state.filter.applyFilter('second-state'); + sut.changeContext(os); + // assert + expectExists(sut.state.filter.currentFilter); + const actualFilter = sut.state.filter.currentFilter.query; + expect(actualFilter).to.equal(expectedFilter); + }); + describe('contextChanged', () => { + it('fired as expected on change', () => { + // arrange + const nextOs = OperatingSystem.macOS; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.Windows, nextOs); + const expectedCollection = testContext.app.getCollection(OperatingSystem.macOS); + // act + const sut = testContext + .withInitialOs(OperatingSystem.Windows) + .construct(); + const oldState = sut.state; + sut.changeContext(nextOs); + // assert + expect(testContext.firedEvents.length).to.equal(1); + expect(testContext.firedEvents[0].newState).to.equal(sut.state); + expect(testContext.firedEvents[0].newState.collection).to.equal(expectedCollection); + expect(testContext.firedEvents[0].oldState).to.equal(oldState); + }); + it('is not fired when initial os is changed to same one', () => { + // arrange + const os = OperatingSystem.Windows; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(os); + // act + const sut = testContext + .withInitialOs(os) + .construct(); + const initialState = sut.state; + initialState.filter.applyFilter('dirty-state'); + sut.changeContext(os); + // assert + expect(testContext.firedEvents.length).to.equal(0); + }); + it('new event is fired for each change', () => { + // arrange + const os = OperatingSystem.Windows; + const changedOs = OperatingSystem.macOS; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(os, changedOs); + // act + const sut = testContext + .withInitialOs(os) + .construct(); + sut.changeContext(changedOs); + sut.changeContext(os); + sut.changeContext(changedOs); + // assert + const duplicates = getDuplicates(testContext.firedEvents); + expect(duplicates.length).to.be.equal(0); + }); + }); + }); + describe('ctor', () => { + describe('collection', () => { + it('returns right collection for expected OS', () => { + // arrange + const os = OperatingSystem.Windows; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(os); + const expected = testContext.app.getCollection(os); + // act + const sut = testContext + .withInitialOs(os) + .construct(); + // assert + const actual = sut.state.collection; + expect(actual).to.deep.equal(expected); + }); + }); + describe('state', () => { + it('initially returns an empty state', () => { + // arrange + const sut = new ObservableApplicationContextFactory() + .construct(); + // act + const actual = sut.state; + // assert + expectEmptyState(actual); + }); + }); + describe('os', () => { + it('set as initial OS', () => { + // arrange + const expected = OperatingSystem.Windows; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.macOS, expected); + // act + const sut = testContext + .withInitialOs(expected) + .construct(); + // assert + const actual = sut.state.os; + expect(actual).to.deep.equal(expected); + }); + it('throws when OS is unknown to application', () => { + // arrange + const expectedError = 'expected error from application'; + const applicationStub = new ApplicationStub(); + applicationStub.getCollection = () => { throw new Error(expectedError); }; + // act + const act = () => new ObservableApplicationContextFactory() + .withApp(applicationStub) + .construct(); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('app', () => { + it('sets app as expected', () => { + // arrange + const os = OperatingSystem.Windows; + const expected = new ApplicationStub().withCollection( + new CategoryCollectionStub().withOs(os), + ); + // act + const sut = new ObservableApplicationContextFactory() + .withApp(expected) + .withInitialOs(os) + .construct(); + // assert + expect(expected).to.equal(sut.app); + }); + }); + }); +}); + +class ObservableApplicationContextFactory { + private static DefaultOs = OperatingSystem.Windows; + + public app: IApplication; + + public firedEvents = new Array(); + + private initialOs = ObservableApplicationContextFactory.DefaultOs; + + constructor() { + this.withAppContainingCollections(ObservableApplicationContextFactory.DefaultOs); + } + + public withAppContainingCollections( + ...oses: OperatingSystem[] + ): ObservableApplicationContextFactory { + const collectionValues = oses.map((os) => new CategoryCollectionStub().withOs(os)); + const app = new ApplicationStub().withCollections(...collectionValues); + return this.withApp(app); + } + + public withApp(app: IApplication): ObservableApplicationContextFactory { + this.app = app; + return this; + } + + public withInitialOs(initialOs: OperatingSystem) { + this.initialOs = initialOs; + return this; + } + + public construct(): IApplicationContext { + const sut = new ApplicationContext(this.app, this.initialOs); + sut.contextChanged.on((newContext) => this.firedEvents.push(newContext)); + return sut; + } +} +function getDuplicates(list: readonly T[]): T[] { + return list.filter((item, index) => list.indexOf(item) !== index); +} + +function expectEmptyState(state: ICategoryCollectionState) { + expect(!state.code.current); + expect(!state.filter.currentFilter); + expect(!state.selection); +} diff --git a/tests/unit/application/Context/ApplicationContextFactory.spec.ts b/tests/unit/application/Context/ApplicationContextFactory.spec.ts new file mode 100644 index 00000000..6909ef90 --- /dev/null +++ b/tests/unit/application/Context/ApplicationContextFactory.spec.ts @@ -0,0 +1,102 @@ +import { describe, it, expect } from 'vitest'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import { buildContext } from '@/application/Context/ApplicationContextFactory'; +import type { IApplicationFactory } from '@/application/IApplicationFactory'; +import type { IApplication } from '@/domain/IApplication'; +import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub'; +import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; + +describe('ApplicationContextFactory', () => { + describe('buildContext', () => { + describe('factory', () => { + it('sets application from factory', async () => { + // arrange + const expected = new ApplicationStub().withCollection( + new CategoryCollectionStub().withOs(OperatingSystem.macOS), + ); + const factoryMock = mockFactoryWithApp(expected); + // act + const context = await buildContext(factoryMock); + // assert + expect(expected).to.equal(context.app); + }); + }); + describe('environment', () => { + describe('sets initial OS as expected', () => { + it('returns current OS if it is supported', async () => { + // arrange + const expected = OperatingSystem.Windows; + const environment = new RuntimeEnvironmentStub().withOs(expected); + const collection = new CategoryCollectionStub().withOs(expected); + const factoryMock = mockFactoryWithCollection(collection); + // act + const context = await buildContext(factoryMock, environment); + // assert + const actual = context.state.os; + expect(expected).to.equal(actual); + }); + it('fallbacks to other OS if OS in environment is not supported', async () => { + // arrange + const expected = OperatingSystem.Windows; + const currentOs = OperatingSystem.macOS; + const environment = new RuntimeEnvironmentStub().withOs(currentOs); + const collection = new CategoryCollectionStub().withOs(expected); + const factoryMock = mockFactoryWithCollection(collection); + // act + const context = await buildContext(factoryMock, environment); + // assert + const actual = context.state.os; + expect(expected).to.equal(actual); + }); + it('fallbacks to most supported OS if current OS is not supported', async () => { + // arrange + const runtimeOs = OperatingSystem.macOS; + const expectedOs = OperatingSystem.Android; + const allCollections = [ + new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3), + new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5), + new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4), + ]; + const environment = new RuntimeEnvironmentStub().withOs(runtimeOs); + const app = new ApplicationStub().withCollections(...allCollections); + const factoryMock = mockFactoryWithApp(app); + // act + const context = await buildContext(factoryMock, environment); + // assert + const actual = context.state.os; + expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`); + }); + it('fallbacks to most supported OS if current OS is undefined', async () => { + // arrange + const runtimeOs = OperatingSystem.macOS; + const expectedOs = OperatingSystem.Android; + const allCollections = [ + new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3), + new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5), + new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4), + ]; + const environment = new RuntimeEnvironmentStub().withOs(runtimeOs); + const app = new ApplicationStub().withCollections(...allCollections); + const factoryMock = mockFactoryWithApp(app); + // act + const context = await buildContext(factoryMock, environment); + // assert + const actual = context.state.os; + expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`); + }); + }); + }); + }); +}); + +function mockFactoryWithCollection(result: ICategoryCollection): IApplicationFactory { + return mockFactoryWithApp(new ApplicationStub().withCollection(result)); +} + +function mockFactoryWithApp(app: IApplication): IApplicationFactory { + return { + getApp: () => Promise.resolve(app), + }; +} diff --git a/tests/unit/application/Context/State/CategoryCollectionState.spec.ts b/tests/unit/application/Context/State/CategoryCollectionState.spec.ts new file mode 100644 index 00000000..cd91a9e6 --- /dev/null +++ b/tests/unit/application/Context/State/CategoryCollectionState.spec.ts @@ -0,0 +1,167 @@ +import { describe, it, expect } from 'vitest'; +import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub'; +import { CategoryCollectionState } from '@/application/Context/State/CategoryCollectionState'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; +import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import { ApplicationCodeStub } from '@tests/unit/shared/Stubs/ApplicationCodeStub'; +import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import type { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; +import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import type { CodeFactory, FilterFactory, SelectionFactory } from '@/application/Context/State/CategoryCollectionState'; +import { FilterContextStub } from '@tests/unit/shared/Stubs/FilterContextStub'; + +describe('CategoryCollectionState', () => { + describe('code', () => { + it('uses the correct scripting definition', () => { + // arrange + const expectedScripting = new ScriptingDefinitionStub(); + const collection = new CategoryCollectionStub() + .withScripting(expectedScripting); + let actualScripting: IScriptingDefinition | undefined; + const codeFactoryMock: CodeFactory = (_, scripting) => { + actualScripting = scripting; + return new ApplicationCodeStub(); + }; + // act + new CategoryCollectionStateBuilder() + .withCollection(collection) + .withCodeFactory(codeFactoryMock) + .build(); + // assert + expectExists(actualScripting); + expect(actualScripting).to.equal(expectedScripting); + }); + it('initializes with the expected script selection', () => { + // arrange + const expectedScriptSelection = new ScriptSelectionStub(); + const selectionFactoryMock: SelectionFactory = () => { + return new UserSelectionStub().withScripts(expectedScriptSelection); + }; + let actualScriptSelection: ReadonlyScriptSelection | undefined; + const codeFactoryMock: CodeFactory = (scriptSelection) => { + actualScriptSelection = scriptSelection; + return new ApplicationCodeStub(); + }; + // act + new CategoryCollectionStateBuilder() + .withCodeFactory(codeFactoryMock) + .withSelectionFactory(selectionFactoryMock) + .build(); + // assert + expectExists(actualScriptSelection); + expect(actualScriptSelection).to.equal(expectedScriptSelection); + }); + }); + describe('os', () => { + it('matches the operating system of the collection', () => { + // arrange + const expected = OperatingSystem.macOS; + const collection = new CategoryCollectionStub() + .withOs(expected); + // act + const sut = new CategoryCollectionStateBuilder() + .withCollection(collection) + .build(); + // assert + const actual = sut.os; + expect(expected).to.equal(actual); + }); + }); + describe('selection', () => { + it('initializes with empty scripts', () => { + // arrange + const expectedScripts = []; + let actualScripts: readonly SelectedScript[] | undefined; + const selectionFactoryMock: SelectionFactory = (_, scripts) => { + actualScripts = scripts; + return new UserSelectionStub(); + }; + // act + new CategoryCollectionStateBuilder() + .withSelectionFactory(selectionFactoryMock) + .build(); + // assert + expectExists(actualScripts); + expect(actualScripts).to.deep.equal(expectedScripts); + }); + it('initializes with the provided collection', () => { + // arrange + const expectedCollection = new CategoryCollectionStub(); + let actualCollection: ICategoryCollection | undefined; + const selectionFactoryMock: SelectionFactory = (collection) => { + actualCollection = collection; + return new UserSelectionStub(); + }; + // act + new CategoryCollectionStateBuilder() + .withCollection(expectedCollection) + .withSelectionFactory(selectionFactoryMock) + .build(); + // assert + expectExists(actualCollection); + expect(actualCollection).to.equal(expectedCollection); + }); + }); + describe('filter', () => { + it('initializes with the provided collection for filtering', () => { + // arrange + const expectedCollection = new CategoryCollectionStub(); + let actualCollection: ICategoryCollection | undefined; + const filterFactoryMock: FilterFactory = (collection) => { + actualCollection = collection; + return new FilterContextStub(); + }; + // act + new CategoryCollectionStateBuilder() + .withCollection(expectedCollection) + .withFilterFactory(filterFactoryMock) + .build(); + // assert + expectExists(expectedCollection); + expect(expectedCollection).to.equal(actualCollection); + }); + }); +}); + +class CategoryCollectionStateBuilder { + private collection: ICategoryCollection = new CategoryCollectionStub(); + + private codeFactory: CodeFactory = () => new ApplicationCodeStub(); + + private selectionFactory: SelectionFactory = () => new UserSelectionStub(); + + private filterFactory: FilterFactory = () => new FilterContextStub(); + + public withCollection(collection: ICategoryCollection): this { + this.collection = collection; + return this; + } + + public withCodeFactory(codeFactory: CodeFactory): this { + this.codeFactory = codeFactory; + return this; + } + + public withSelectionFactory(selectionFactory: SelectionFactory): this { + this.selectionFactory = selectionFactory; + return this; + } + + public withFilterFactory(filterFactory: FilterFactory): this { + this.filterFactory = filterFactory; + return this; + } + + public build() { + return new CategoryCollectionState( + this.collection, + this.selectionFactory, + this.codeFactory, + this.filterFactory, + ); + } +} diff --git a/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts b/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts new file mode 100644 index 00000000..a555ab7b --- /dev/null +++ b/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts @@ -0,0 +1,210 @@ +import { describe, it, expect } from 'vitest'; +import { ApplicationCode } from '@/application/Context/State/Code/ApplicationCode'; +import type { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent'; +import type { IUserScriptGenerator } from '@/application/Context/State/Code/Generation/IUserScriptGenerator'; +import { CodePosition } from '@/application/Context/State/Code/Position/CodePosition'; +import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; +import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import type { IUserScript } from '@/application/Context/State/Code/Generation/IUserScript'; +import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub'; + +describe('ApplicationCode', () => { + describe('ctor', () => { + it('empty when selection is empty', () => { + // arrange + const selectedScripts = []; + const selection = new ScriptSelectionStub() + .withSelectedScripts(selectedScripts); + const definition = new ScriptingDefinitionStub(); + const sut = new ApplicationCode(selection, definition); + // act + const actual = sut.current; + // assert + expect(actual).to.have.lengthOf(0); + }); + it('generates code from script generator when selection is not empty', () => { + // arrange + const scripts = [new ScriptStub('first'), new ScriptStub('second')]; + const selectedScripts = scripts.map((script) => script.toSelectedScript()); + const selection = new ScriptSelectionStub() + .withSelectedScripts(selectedScripts); + const definition = new ScriptingDefinitionStub(); + const expected: IUserScript = { + code: 'expected-code', + scriptPositions: new Map(), + }; + const generator = new UserScriptGeneratorMock() + .plan({ scripts: selection.selectedScripts, definition }, expected); + const sut = new ApplicationCode(selection, definition, generator); + // act + const actual = sut.current; + // assert + expect(actual).to.equal(expected.code); + }); + }); + describe('changed event', () => { + describe('code', () => { + it('empty when nothing is selected', () => { + // arrange + let signaled: ICodeChangedEvent | undefined; + const scripts = [new ScriptStub('first'), new ScriptStub('second')]; + const selectedScripts = scripts.map((script) => script.toSelectedScript()); + const selection = new ScriptSelectionStub() + .withSelectedScripts(selectedScripts); + const definition = new ScriptingDefinitionStub(); + const sut = new ApplicationCode(selection, definition); + sut.changed.on((code) => { + signaled = code; + }); + // act + selection.changed.notify([]); + // assert + expectExists(signaled); + expect(signaled.code).to.have.lengthOf(0); + expect(signaled.code).to.equal(sut.current); + }); + it('has code when some are selected', () => { + // arrange + let signaled: ICodeChangedEvent | undefined; + const scripts = [new ScriptStub('first'), new ScriptStub('second')]; + const selectedScripts = scripts.map( + (script) => script.toSelectedScript().withRevert(false), + ); + const selection = new ScriptSelectionStub() + .withSelectedScripts(selectedScripts); + const definition = new ScriptingDefinitionStub(); + const sut = new ApplicationCode(selection, definition); + sut.changed.on((code) => { + signaled = code; + }); + // act + selection.changed.notify(selectedScripts); + // assert + expectExists(signaled); + expect(signaled.code).to.have.length.greaterThan(0); + expect(signaled.code).to.equal(sut.current); + }); + }); + describe('calls UserScriptGenerator', () => { + it('sends scripting definition to generator', () => { + // arrange + const expectedDefinition = new ScriptingDefinitionStub(); + const selection = new ScriptSelectionStub() + .withSelectedScripts([]); + const generatorMock: IUserScriptGenerator = { + buildCode: (_, definition) => { + if (definition !== expectedDefinition) { + throw new Error('Unexpected scripting definition'); + } + return { + code: 'non-important-code', + scriptPositions: new Map(), + }; + }, + }; + // eslint-disable-next-line no-new + new ApplicationCode(selection, expectedDefinition, generatorMock); + // act + const act = () => selection.changed.notify([]); + // assert + expect(act).to.not.throw(); + }); + it('sends selected scripts to generator', () => { + // arrange + const expectedDefinition = new ScriptingDefinitionStub(); + const scripts = [new ScriptStub('first'), new ScriptStub('second')]; + const selectedScripts = scripts.map((script) => script.toSelectedScript()); + const selection = new ScriptSelectionStub() + .withSelectedScripts(selectedScripts); + const generatorMock: IUserScriptGenerator = { + buildCode: (actualScripts) => { + if (JSON.stringify(actualScripts) !== JSON.stringify(selectedScripts)) { + throw new Error('Unexpected scripts'); + } + return { + code: '', + scriptPositions: new Map(), + }; + }, + }; + // eslint-disable-next-line no-new + new ApplicationCode(selection, expectedDefinition, generatorMock); + // act + const act = () => selection.changed.notify(selectedScripts); + // assert + expect(act).to.not.throw(); + }); + it('sets positions from the generator', () => { + // arrange + let signaled: ICodeChangedEvent | undefined; + const scripts = [new ScriptStub('first'), new ScriptStub('second')]; + const selectedScripts = scripts.map( + (script) => script.toSelectedScript().withRevert(false), + ); + const selection = new ScriptSelectionStub() + .withSelectedScripts(selectedScripts); + const scriptingDefinition = new ScriptingDefinitionStub(); + const totalLines = 20; + const expected = new Map( + [ + [selectedScripts[0], new CodePosition(0, totalLines / 2)], + [selectedScripts[1], new CodePosition(totalLines / 2, totalLines)], + ], + ); + const generatorMock: IUserScriptGenerator = { + buildCode: () => { + return { + code: '\nREM LINE'.repeat(totalLines), + scriptPositions: expected, + }; + }, + }; + const sut = new ApplicationCode(selection, scriptingDefinition, generatorMock); + sut.changed.on((code) => { + signaled = code; + }); + // act + selection.changed.notify(selectedScripts); + // assert + expectExists(signaled); + expect(signaled.getScriptPositionInCode(scripts[0])) + .to.deep.equal(expected.get(selectedScripts[0])); + expect(signaled.getScriptPositionInCode(scripts[1])) + .to.deep.equal(expected.get(selectedScripts[1])); + }); + }); + }); +}); + +interface ScriptGenerationParameters { + readonly scripts: readonly SelectedScript[]; + readonly definition: IScriptingDefinition; +} +class UserScriptGeneratorMock implements IUserScriptGenerator { + private prePlanned = new Map(); + + public plan( + parameters: ScriptGenerationParameters, + result: IUserScript, + ): UserScriptGeneratorMock { + this.prePlanned.set(parameters, result); + return this; + } + + public buildCode( + selectedScripts: readonly SelectedScript[], + scriptingDefinition: IScriptingDefinition, + ): IUserScript { + for (const [parameters, result] of this.prePlanned) { + if (selectedScripts === parameters.scripts + && scriptingDefinition === parameters.definition) { + return result; + } + } + throw new Error('Unexpected parameters'); + } +} diff --git a/tests/unit/application/Context/State/Code/Event/CodeChangedEvent.spec.ts b/tests/unit/application/Context/State/Code/Event/CodeChangedEvent.spec.ts new file mode 100644 index 00000000..b4266346 --- /dev/null +++ b/tests/unit/application/Context/State/Code/Event/CodeChangedEvent.spec.ts @@ -0,0 +1,262 @@ +import { describe, it, expect } from 'vitest'; +import { CodeChangedEvent } from '@/application/Context/State/Code/Event/CodeChangedEvent'; +import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; +import { CodePosition } from '@/application/Context/State/Code/Position/CodePosition'; +import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector'; + +describe('CodeChangedEvent', () => { + describe('ctor', () => { + describe('position validation', () => { + it('throws when code position is out of range', () => { + // arrange + const code = 'singleline code'; + const nonExistingLine1 = 2; + const nonExistingLine2 = 31; + const newScripts = new Map([ + [new SelectedScriptStub(new ScriptStub('1')), new CodePosition(0, nonExistingLine1)], + [new SelectedScriptStub(new ScriptStub('2')), new CodePosition(0, nonExistingLine2)], + ]); + // act + const actualErrorMessage = collectExceptionMessage(() => { + new CodeChangedEventBuilder() + .withCode(code) + .withNewScripts(newScripts) + .build(); + }); + // assert + expect(actualErrorMessage).to.include(nonExistingLine1); + expect(actualErrorMessage).to.include(nonExistingLine2); + }); + it('invalid line position validation counts empty lines', () => { + // arrange + const totalEmptyLines = 5; + const code = '\n'.repeat(totalEmptyLines); + // If empty lines would not be counted, this would result in error + const existingLineEnd = totalEmptyLines; + const newScripts = new Map([ + [new SelectedScriptStub(new ScriptStub('1')), new CodePosition(0, existingLineEnd)], + ]); + // act + const act = () => { + new CodeChangedEventBuilder() + .withCode(code) + .withNewScripts(newScripts) + .build(); + }; + // assert + expect(act).to.not.throw(); + }); + describe('does not throw with valid code position', () => { + // arrange + const testCases = [ + { + name: 'singleline', + code: 'singleline code', + position: new CodePosition(0, 1), + }, + { + name: 'multiline', + code: 'multiline code\nsecond line', + position: new CodePosition(0, 2), + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + const newScripts = new Map([ + [new SelectedScriptStub(new ScriptStub('1')), testCase.position], + ]); + // act + const act = () => new CodeChangedEventBuilder() + .withCode(testCase.code) + .withNewScripts(newScripts) + .build(); + // assert + expect(act).to.not.throw(); + }); + } + }); + }); + }); + it('code returns expected', () => { + // arrange + const expected = 'code'; + const sut = new CodeChangedEventBuilder() + .withCode(expected) + .build(); + // act + const actual = sut.code; + // assert + expect(actual).to.equal(expected); + }); + describe('addedScripts', () => { + it('returns new scripts when scripts are added', () => { + // arrange + const expected = [new ScriptStub('3'), new ScriptStub('4')]; + const initialScripts = [ + new SelectedScriptStub(new ScriptStub('1')), + new SelectedScriptStub(new ScriptStub('2')), + ]; + const newScripts = new Map([ + [initialScripts[0], new CodePosition(0, 1)], + [initialScripts[1], new CodePosition(0, 1)], + [new SelectedScriptStub(expected[0]).withRevert(false), new CodePosition(0, 1)], + [new SelectedScriptStub(expected[1]).withRevert(false), new CodePosition(0, 1)], + ]); + const sut = new CodeChangedEventBuilder() + .withOldScripts(initialScripts) + .withNewScripts(newScripts) + .build(); + // act + const actual = sut.addedScripts; + // assert + expect(actual).to.have.lengthOf(2); + expect(actual[0]).to.deep.equal(expected[0]); + expect(actual[1]).to.deep.equal(expected[1]); + }); + }); + describe('removedScripts', () => { + it('returns removed scripts when script are removed', () => { + // arrange + const existingScripts = [ + new SelectedScriptStub(new ScriptStub('0')), + new SelectedScriptStub(new ScriptStub('1')), + ]; + const removedScripts = [ + new SelectedScriptStub(new ScriptStub('2')), + ]; + const initialScripts = [...existingScripts, ...removedScripts]; + const newScripts = new Map([ + [initialScripts[0], new CodePosition(0, 1)], + [initialScripts[1], new CodePosition(0, 1)], + ]); + const sut = new CodeChangedEventBuilder() + .withOldScripts(initialScripts) + .withNewScripts(newScripts) + .build(); + // act + const actual = sut.removedScripts; + // assert + expect(actual).to.have.lengthOf(removedScripts.length); + expect(actual[0]).to.deep.equal(removedScripts[0].script); + }); + }); + describe('changedScripts', () => { + it('returns changed scripts when scripts are changed', () => { + // arrange + const changedScripts = [ + new ScriptStub('scripts-with-changed-selection-1'), + new ScriptStub('scripts-with-changed-selection-2'), + ]; + const initialScripts = [ + new SelectedScriptStub(changedScripts[0]).withRevert(false), + new SelectedScriptStub(changedScripts[1]).withRevert(false), + ]; + const newScripts = new Map([ + [new SelectedScriptStub(changedScripts[0]).withRevert(true), new CodePosition(0, 1)], + [new SelectedScriptStub(changedScripts[1]).withRevert(false), new CodePosition(0, 1)], + ]); + const sut = new CodeChangedEventBuilder() + .withOldScripts(initialScripts) + .withNewScripts(newScripts) + .build(); + // act + const actual = sut.changedScripts; + // assert + expect(actual).to.have.lengthOf(1); + expect(actual[0]).to.deep.equal(initialScripts[0].script); + }); + }); + describe('isEmpty', () => { + it('returns true when empty', () => { + // arrange + const newScripts = new Map(); + const oldScripts = [new SelectedScriptStub(new ScriptStub('1')).withRevert(false)]; + const sut = new CodeChangedEventBuilder() + .withOldScripts(oldScripts) + .withNewScripts(newScripts) + .build(); + // act + const actual = sut.isEmpty(); + // assert + expect(actual).to.equal(true); + }); + it('returns false when not empty', () => { + // arrange + const oldScripts = [new SelectedScriptStub(new ScriptStub('1'))]; + const newScripts = new Map([ + [oldScripts[0], new CodePosition(0, 1)], + ]); + const sut = new CodeChangedEventBuilder() + .withOldScripts(oldScripts) + .withNewScripts(newScripts) + .build(); + // act + const actual = sut.isEmpty(); + // assert + expect(actual).to.equal(false); + }); + }); + describe('getScriptPositionInCode', () => { + it('throws if script is unknown', () => { + // arrange + const expectedError = 'Unknown script: Position could not be found for the script'; + const unknownScript = new ScriptStub('1'); + const sut = new CodeChangedEventBuilder() + .build(); + // act + const act = () => sut.getScriptPositionInCode(unknownScript); + // assert + expect(act).to.throw(expectedError); + }); + it('returns expected position for existing script', () => { + // arrange + const script = new ScriptStub('1'); + const expected = new CodePosition(0, 1); + const newScripts = new Map([ + [new SelectedScriptStub(script).withRevert(false), expected], + ]); + const sut = new CodeChangedEventBuilder() + .withNewScripts(newScripts) + .build(); + // act + const actual = sut.getScriptPositionInCode(script); + // assert + expect(actual).to.deep.equal(expected); + }); + }); +}); + +// Prefer over original ctor in tests for easier future ctor refactorings +class CodeChangedEventBuilder { + private code = '[CodeChangedEventBuilder] default code'; + + private oldScripts: ReadonlyArray = []; + + private newScripts = new Map(); + + public withCode(code: string) { + this.code = code; + return this; + } + + public withOldScripts(oldScripts: ReadonlyArray) { + this.oldScripts = oldScripts; + return this; + } + + public withNewScripts(newScripts: Map) { + this.newScripts = newScripts; + return this; + } + + public build(): CodeChangedEvent { + return new CodeChangedEvent( + this.code, + this.oldScripts, + this.newScripts, + ); + } +} diff --git a/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts b/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts new file mode 100644 index 00000000..43676f1d --- /dev/null +++ b/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts @@ -0,0 +1,197 @@ +import { describe, it, expect } from 'vitest'; +import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder'; +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; + +describe('CodeBuilder', () => { + class CodeBuilderConcrete extends CodeBuilder { + private commentDelimiter = '//'; + + private newLineTerminator = '\n'; + + public withCommentDelimiter(delimiter: string): CodeBuilderConcrete { + this.commentDelimiter = delimiter; + return this; + } + + public withNewLineTerminator(terminator: string): CodeBuilderConcrete { + this.newLineTerminator = terminator; + return this; + } + + protected getCommentDelimiter(): string { + return this.commentDelimiter; + } + + protected writeStandardOut(text: string): string { + return text; + } + + protected getNewLineTerminator(): string { + return this.newLineTerminator; + } + } + describe('appendLine', () => { + it('when empty appends empty line', () => { + // arrange + const sut = new CodeBuilderConcrete(); + // act + sut.appendLine().appendLine().appendLine(); + // assert + expect(sut.toString()).to.equal('\n\n'); + }); + it('when not empty append string in new line', () => { + // arrange + const sut = new CodeBuilderConcrete(); + const expected = 'str'; + // act + sut.appendLine() + .appendLine(expected); + // assert + const result = sut.toString(); + const lines = splitTextIntoLines(result); + expect(lines[1]).to.equal('str'); + }); + describe('append multi-line string correctly', () => { + it('appends multi-line string with empty lines preserved', () => { + // arrange + const sut = new CodeBuilderConcrete(); + const expectedLines: string[] = ['', 'line1', '', 'line2', '', '', 'line3', '', '']; + const multilineInput = expectedLines.join('\n'); + + // act + sut.appendLine(multilineInput); + const actual = sut.toString(); + + // assert + const actualLines = splitTextIntoLines(actual); + expect(actualLines).to.deep.equal(expectedLines); + }); + describe('recognizes different line terminators', () => { + const delimiters = ['\n', '\r\n', '\r']; + for (const delimiter of delimiters) { + it(`using "${JSON.stringify(delimiter)}"`, () => { + // arrange + const line1 = 'line1'; + const line2 = 'line2'; + const code = `${line1}${delimiter}${line2}`; + const sut = new CodeBuilderConcrete(); + // act + sut.appendLine(code); + // assert + const result = sut.toString(); + const lines = splitTextIntoLines(result); + expect(lines).to.have.lengthOf(2); + expect(lines[0]).to.equal(line1); + expect(lines[1]).to.equal(line2); + }); + } + }); + it('normalizes different line terminators', () => { + // arrange + const lineTerminator = '🐒'; + const lines = ['line1', 'line2', 'line3', 'line4']; + const code = `${lines[0]}\n${lines[1]}\r\n${lines[2]}\r${lines[3]}`; + const expected = `${lines[0]}${lineTerminator}${lines[1]}${lineTerminator}${lines[2]}${lineTerminator}${lines[3]}`; + const sut = new CodeBuilderConcrete() + .withNewLineTerminator(lineTerminator); + // act + const actual = sut + .appendLine(code) + .toString(); + // assert + expect(actual).to.equal(expected); + }); + }); + }); + it('appendFunction', () => { + // arrange + const sut = new CodeBuilderConcrete(); + const functionName = 'expected-function-name'; + const code = 'expected-code'; + // act + sut.appendFunction(functionName, code); + // assert + const result = sut.toString(); + expect(result).to.include(functionName); + expect(result).to.include(code); + }); + it('appendTrailingHyphensCommentLine', () => { + // arrange + const commentDelimiter = '//'; + const sut = new CodeBuilderConcrete() + .withCommentDelimiter(commentDelimiter); + const totalHyphens = 5; + const expected = `${commentDelimiter} ${'-'.repeat(totalHyphens)}`; + // act + sut.appendTrailingHyphensCommentLine(totalHyphens); + // assert + const result = sut.toString(); + const lines = splitTextIntoLines(result); + expect(lines[0]).to.equal(expected); + }); + it('appendCommentLine', () => { + // arrange + const commentDelimiter = '//'; + const sut = new CodeBuilderConcrete() + .withCommentDelimiter(commentDelimiter); + const comment = 'comment'; + const expected = `${commentDelimiter} comment`; + // act + const result = sut + .appendCommentLine(comment) + .toString(); + // assert + const lines = splitTextIntoLines(result); + expect(lines[0]).to.equal(expected); + }); + it('appendCommentLineWithHyphensAround', () => { + // arrange + const commentDelimiter = '//'; + const sut = new CodeBuilderConcrete() + .withCommentDelimiter(commentDelimiter); + const sectionName = 'section'; + const totalHyphens = sectionName.length + 3 * 2; + const expected = `${commentDelimiter} ---section---`; + // act + const result = sut + .appendCommentLineWithHyphensAround(sectionName, totalHyphens) + .toString(); + // assert + const lines = splitTextIntoLines(result); + expect(lines[1]).to.equal(expected); + }); + describe('currentLine', () => { + it('no lines returns zero', () => { + // arrange & act + const sut = new CodeBuilderConcrete(); + // assert + expect(sut.currentLine).to.equal(0); + }); + it('single line returns one', () => { + // arrange + const sut = new CodeBuilderConcrete(); + // act + sut.appendLine(); + // assert + expect(sut.currentLine).to.equal(1); + }); + it('multiple lines returns as expected', () => { + // arrange + const sut = new CodeBuilderConcrete(); + // act + sut.appendLine('1') + .appendCommentLine('2') + .appendLine(); + // assert + expect(sut.currentLine).to.equal(3); + }); + it('multiple lines in code', () => { + // arrange + const sut = new CodeBuilderConcrete(); + // act + sut.appendLine('hello\ncode-here\nwith-3-lines'); + // assert + expect(sut.currentLine).to.equal(3); + }); + }); +}); diff --git a/tests/unit/application/Context/State/Code/Generation/CodeBuilderFactory.spec.ts b/tests/unit/application/Context/State/Code/Generation/CodeBuilderFactory.spec.ts new file mode 100644 index 00000000..c568f578 --- /dev/null +++ b/tests/unit/application/Context/State/Code/Generation/CodeBuilderFactory.spec.ts @@ -0,0 +1,14 @@ +import { describe } from 'vitest'; +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import { ShellBuilder } from '@/application/Context/State/Code/Generation/Languages/ShellBuilder'; +import { BatchBuilder } from '@/application/Context/State/Code/Generation/Languages/BatchBuilder'; +import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory'; +import { ScriptingLanguageFactoryTestRunner } from '@tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner'; + +describe('CodeBuilderFactory', () => { + const sut = new CodeBuilderFactory(); + const runner = new ScriptingLanguageFactoryTestRunner() + .expectInstance(ScriptingLanguage.shellscript, ShellBuilder) + .expectInstance(ScriptingLanguage.batchfile, BatchBuilder); + runner.testCreateMethod(sut); +}); diff --git a/tests/unit/application/Context/State/Code/Generation/Languages/BatchBuilder.spec.ts b/tests/unit/application/Context/State/Code/Generation/Languages/BatchBuilder.spec.ts new file mode 100644 index 00000000..b86d2ce5 --- /dev/null +++ b/tests/unit/application/Context/State/Code/Generation/Languages/BatchBuilder.spec.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { BatchBuilder } from '@/application/Context/State/Code/Generation/Languages/BatchBuilder'; + +describe('BatchBuilder', () => { + class BatchBuilderRevealer extends BatchBuilder { + public getCommentDelimiter(): string { + return super.getCommentDelimiter(); + } + + public writeStandardOut(text: string): string { + return super.writeStandardOut(text); + } + + public getNewLineTerminator(): string { + return super.getNewLineTerminator(); + } + } + describe('getCommentDelimiter', () => { + it('returns expected', () => { + // arrange + const expected = '::'; + const sut = new BatchBuilderRevealer(); + // act + const actual = sut.getCommentDelimiter(); + // assert + expect(expected).to.equal(actual); + }); + }); + describe('writeStandardOut', () => { + const testData = [ + { + name: 'plain text', + text: 'test', + expected: 'echo test', + }, + { + name: 'text with ampersand', + text: 'a & b', + expected: 'echo a ^& b', + }, + { + name: 'text with percent sign', + text: '90%', + expected: 'echo 90%%', + }, + { + name: 'text with multiple ampersands and percent signs', + text: 'Me&you in % ? You & me = 0%', + expected: 'echo Me^&you in %% ? You ^& me = 0%%', + }, + ]; + for (const test of testData) { + it(test.name, () => { + // arrange + const sut = new BatchBuilderRevealer(); + // act + const actual = sut.writeStandardOut(test.text); + // assert + expect(test.expected).to.equal(actual); + }); + } + }); + describe('getNewLineTerminator', () => { + it('returns expected', () => { + // arrange + const expected = '\r\n'; + const sut = new BatchBuilderRevealer(); + // act + const actual = sut.getNewLineTerminator(); + // assert + expect(expected).to.equal(actual); + }); + }); +}); diff --git a/tests/unit/application/Context/State/Code/Generation/Languages/ShellBuilder.spec.ts b/tests/unit/application/Context/State/Code/Generation/Languages/ShellBuilder.spec.ts new file mode 100644 index 00000000..32caef39 --- /dev/null +++ b/tests/unit/application/Context/State/Code/Generation/Languages/ShellBuilder.spec.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from 'vitest'; +import { ShellBuilder } from '@/application/Context/State/Code/Generation/Languages/ShellBuilder'; + +describe('ShellBuilder', () => { + class ShellBuilderRevealer extends ShellBuilder { + public getCommentDelimiter(): string { + return super.getCommentDelimiter(); + } + + public writeStandardOut(text: string): string { + return super.writeStandardOut(text); + } + + public getNewLineTerminator(): string { + return super.getNewLineTerminator(); + } + } + describe('getCommentDelimiter', () => { + it('returns expected', () => { + // arrange + const expected = '#'; + const sut = new ShellBuilderRevealer(); + // act + const actual = sut.getCommentDelimiter(); + // assert + expect(expected).to.equal(actual); + }); + }); + describe('writeStandardOut', () => { + const testData = [ + { + name: 'plain text', + text: 'test', + expected: 'echo \'test\'', + }, + { + name: 'text with single quote', + text: 'I\'m not who you think I am', + expected: 'echo \'I\'\\\'\'m not who you think I am\'', + }, + { + name: 'text with multiple single quotes', + text: 'I\'m what you\'re', + expected: 'echo \'I\'\\\'\'m what you\'\\\'\'re\'', + }, + ]; + for (const test of testData) { + it(test.name, () => { + // arrange + const sut = new ShellBuilderRevealer(); + // act + const actual = sut.writeStandardOut(test.text); + // assert + expect(test.expected).to.equal(actual); + }); + } + }); + describe('getNewLineTerminator', () => { + it('returns expected', () => { + // arrange + const expected = '\n'; + const sut = new ShellBuilderRevealer(); + // act + const actual = sut.getNewLineTerminator(); + // assert + expect(expected).to.equal(actual); + }); + }); +}); diff --git a/tests/unit/application/Context/State/Code/Generation/UserScriptGenerator.spec.ts b/tests/unit/application/Context/State/Code/Generation/UserScriptGenerator.spec.ts new file mode 100644 index 00000000..5a64e98f --- /dev/null +++ b/tests/unit/application/Context/State/Code/Generation/UserScriptGenerator.spec.ts @@ -0,0 +1,274 @@ +import { describe, it, expect } from 'vitest'; +import { UserScriptGenerator } from '@/application/Context/State/Code/Generation/UserScriptGenerator'; +import type { ICodeBuilderFactory } from '@/application/Context/State/Code/Generation/ICodeBuilderFactory'; +import type { ICodeBuilder } from '@/application/Context/State/Code/Generation/ICodeBuilder'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub'; + +describe('UserScriptGenerator', () => { + describe('scriptingDefinition', () => { + describe('startCode', () => { + it('is prepended if not empty', () => { + // arrange + const sut = new UserScriptGenerator(); + const startCode = 'Start\nCode'; + const script = new ScriptStub('id') + .withCode('code\nmulti-lined') + .toSelectedScript(); + const definition = new ScriptingDefinitionStub() + .withStartCode(startCode); + const expectedStart = `${startCode}\n`; + // act + const code = sut.buildCode([script], definition); + // assert + const actual = code.code; + expect(actual.startsWith(expectedStart)); + }); + describe('is not prepended if empty', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const codeBuilderStub = new CodeBuilderStub(); + const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub)); + const script = new ScriptStub('id') + .withCode('code\nmulti-lined') + .toSelectedScript(); + const definition = new ScriptingDefinitionStub() + .withStartCode(absentValue); + const expectedStart = codeBuilderStub + .appendFunction(script.script.name, script.script.code.execute) + .toString(); + // act + const code = sut.buildCode([script], definition); + // assert + const actual = code.code; + expect(actual.startsWith(expectedStart)); + }, { excludeNull: true, excludeUndefined: true }); + }); + }); + describe('endCode', () => { + it('is appended if not empty', () => { + // arrange + const sut = new UserScriptGenerator(); + const endCode = 'End\nCode'; + const script = new ScriptStub('id') + .withCode('code\nmulti-lined') + .toSelectedScript(); + const definition = new ScriptingDefinitionStub() + .withEndCode(endCode); + const expectedEnd = `${endCode}\n`; + // act + const code = sut.buildCode([script], definition); + // assert + const actual = code.code; + expect(actual.endsWith(expectedEnd)); + }); + describe('is not appended if empty', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const codeBuilderStub = new CodeBuilderStub(); + const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub)); + const script = new ScriptStub('id') + .withCode('code\nmulti-lined') + .toSelectedScript(); + const expectedEnd = codeBuilderStub + .appendFunction(script.script.name, script.script.code.execute) + .toString(); + const definition = new ScriptingDefinitionStub() + .withEndCode(absentValue); + // act + const code = sut.buildCode([script], definition); + // assert + const actual = code.code; + expect(actual.endsWith(expectedEnd)); + }, { excludeNull: true, excludeUndefined: true }); + }); + }); + }); + describe('execute', () => { + it('appends non-revert script', () => { + const sut = new UserScriptGenerator(); + // arrange + const scriptName = 'test non-revert script'; + const scriptCode = 'REM nop'; + const script = new ScriptStub('id').withName(scriptName).withCode(scriptCode); + const selectedScripts = [new SelectedScriptStub(script).withRevert(false)]; + const definition = new ScriptingDefinitionStub(); + // act + const actual = sut.buildCode(selectedScripts, definition); + // assert + expect(actual.code).to.include(scriptName); + expect(actual.code).to.not.include(`${scriptName} (revert)`); + expect(actual.code).to.include(scriptCode); + }); + }); + describe('revert', () => { + it('appends revert script', () => { + // arrange + const sut = new UserScriptGenerator(); + const scriptName = 'test non-revert script'; + const scriptCode = 'REM nop'; + const script = new ScriptStub('id') + .withName(scriptName) + .withRevertCode(scriptCode) + .toSelectedScript() + .withRevert(true); + const definition = new ScriptingDefinitionStub(); + // act + const actual = sut.buildCode([script], definition); + // assert + expect(actual.code).to.include(`${scriptName} (revert)`); + expect(actual.code).to.include(scriptCode); + }); + describe('throws if revert script lacks revert code', () => { + itEachAbsentStringValue((emptyRevertCode) => { + // arrange + const expectedError = 'Reverted script lacks revert code.'; + const sut = new UserScriptGenerator(); + const script = new ScriptStub('id') + .withRevertCode(emptyRevertCode) + .toSelectedScript() + .withRevert(true); + const definition = new ScriptingDefinitionStub(); + // act + const act = () => sut.buildCode([script], definition); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true }); + }); + }); + describe('scriptPositions', () => { + it('without script; returns empty', () => { + // arrange + const sut = new UserScriptGenerator(); + const selectedScripts = []; + const definition = new ScriptingDefinitionStub(); + // act + const actual = sut.buildCode(selectedScripts, definition); + // assert + expect(actual.scriptPositions.size).to.equal(0); + }); + describe('with scripts', () => { + // arrange + const totalStartCodeLines = 2; + const totalFunctionNameLines = 4; + const definition = new ScriptingDefinitionStub() + .withStartCode('First line\nSecond line'); + describe('single script', () => { + const testCases = [ + { + name: 'single-lined', + scriptCode: 'only line', + codeLines: 1, + }, + { + name: 'multi-lined', + scriptCode: 'first line\nsecond line', + codeLines: 2, + }, + ]; + const sut = new UserScriptGenerator(); + for (const testCase of testCases) { + it(testCase.name, () => { + const expectedStartLine = totalStartCodeLines + + 1 // empty line code begin + + 1; // code begin + const expectedEndLine = expectedStartLine + + totalFunctionNameLines + + testCase.codeLines; + const selectedScript = new ScriptStub('script-id') + .withName('script') + .withCode(testCase.scriptCode) + .toSelectedScript() + .withRevert(false); + // act + const actual = sut.buildCode([selectedScript], definition); + // expect + expect(1).to.equal(actual.scriptPositions.size); + const position = actual.scriptPositions.get(selectedScript); + expectExists(position); + expect(expectedStartLine).to.equal(position.startLine, 'Unexpected start line position'); + expect(expectedEndLine).to.equal(position.endLine, 'Unexpected end line position'); + }); + } + }); + it('multiple scripts', () => { + const sut = new UserScriptGenerator(); + const selectedScripts = [ + new ScriptStub('1').withCode('only line'), + new ScriptStub('2').withCode('first line\nsecond line'), + ].map((s) => s.toSelectedScript()); + const expectedFirstScriptStart = totalStartCodeLines + + 1 // empty line code begin + + 1; // code begin + const expectedFirstScriptEnd = expectedFirstScriptStart + + totalFunctionNameLines + + 1; // total code lines + const expectedSecondScriptStart = expectedFirstScriptEnd + + 1 // code end hyphens + + 1 // new line + + 1; // code begin + const expectedSecondScriptEnd = expectedSecondScriptStart + + totalFunctionNameLines + + 2; // total lines of second script + // act + const actual = sut.buildCode(selectedScripts, definition); + // assert + const firstPosition = actual.scriptPositions.get(selectedScripts[0]); + const secondPosition = actual.scriptPositions.get(selectedScripts[1]); + expect(actual.scriptPositions.size).to.equal(2); + expectExists(firstPosition); + expect(expectedFirstScriptStart).to.equal(firstPosition.startLine, 'Unexpected start line position (first script)'); + expect(expectedFirstScriptEnd).to.equal(firstPosition.endLine, 'Unexpected end line position (first script)'); + expectExists(secondPosition); + expect(expectedSecondScriptStart).to.equal(secondPosition.startLine, 'Unexpected start line position (second script)'); + expect(expectedSecondScriptEnd).to.equal(secondPosition.endLine, 'Unexpected end line position (second script)'); + }); + }); + }); +}); + +function mockCodeBuilderFactory(mock: ICodeBuilder): ICodeBuilderFactory { + return { + create: () => mock, + }; +} + +class CodeBuilderStub implements ICodeBuilder { + public currentLine = 0; + + private text = ''; + + public appendLine(code?: string): ICodeBuilder { + this.text += this.text ? `${code}\n` : code; + this.currentLine++; + return this; + } + + public appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder { + return this.appendLine(`trailing-hyphens-${totalRepeatHyphens}`); + } + + public appendCommentLine(commentLine?: string): ICodeBuilder { + return this.appendLine(`Comment | ${commentLine}`); + } + + public appendCommentLineWithHyphensAround( + sectionName: string, + totalRepeatHyphens: number, + ): ICodeBuilder { + return this.appendLine(`hyphens-around-${totalRepeatHyphens} | Section name: ${sectionName} | hyphens-around-${totalRepeatHyphens}`); + } + + public appendFunction(name: string, code: string): ICodeBuilder { + return this + .appendLine(`Function | Name: ${name}`) + .appendLine(`Function | Code: ${code}`); + } + + public toString(): string { + return this.text; + } +} diff --git a/tests/unit/application/Context/State/Code/Position/CodePosition.spec.ts b/tests/unit/application/Context/State/Code/Position/CodePosition.spec.ts new file mode 100644 index 00000000..5cedcf17 --- /dev/null +++ b/tests/unit/application/Context/State/Code/Position/CodePosition.spec.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest'; +import { CodePosition } from '@/application/Context/State/Code/Position/CodePosition'; + +describe('CodePosition', () => { + describe('ctor', () => { + it('creates with valid parameters', () => { + // arrange + const startPosition = 0; + const endPosition = 5; + // act + const sut = new CodePosition(startPosition, endPosition); + // assert + expect(sut.startLine).to.equal(startPosition); + expect(sut.endLine).to.equal(endPosition); + }); + it('throws with negative start position', () => { + // arrange + const startPosition = -1; + const endPosition = 5; + // act + const getSut = () => new CodePosition(startPosition, endPosition); + // assert + expect(getSut).to.throw('Code cannot start in a negative line'); + }); + it('throws with negative end position', () => { + // arrange + const startPosition = 1; + const endPosition = -5; + // act + const getSut = () => new CodePosition(startPosition, endPosition); + // assert + expect(getSut).to.throw('Code cannot end in a negative line'); + }); + it('throws when start and end position is same', () => { + // arrange + const startPosition = 0; + const endPosition = 0; + // act + const getSut = () => new CodePosition(startPosition, endPosition); + // assert + expect(getSut).to.throw('Empty code'); + }); + it('throws when ends before start', () => { + // arrange + const startPosition = 3; + const endPosition = 2; + // act + const getSut = () => new CodePosition(startPosition, endPosition); + // assert + expect(getSut).to.throw('End line cannot be less than start line'); + }); + }); +}); diff --git a/tests/unit/application/Context/State/Filter/AdaptiveFilterContext.spec.ts b/tests/unit/application/Context/State/Filter/AdaptiveFilterContext.spec.ts new file mode 100644 index 00000000..a8e487bc --- /dev/null +++ b/tests/unit/application/Context/State/Filter/AdaptiveFilterContext.spec.ts @@ -0,0 +1,130 @@ +import { describe, it, expect } from 'vitest'; +import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsStub'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; +import type { FilterChangeDetails } from '@/application/Context/State/Filter/Event/FilterChangeDetails'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { AdaptiveFilterContext } from '@/application/Context/State/Filter/AdaptiveFilterContext'; +import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub'; +import { FilterStrategyStub } from '@tests/unit/shared/Stubs/FilterStrategyStub'; +import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy'; +import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; + +describe('AdaptiveFilterContext', () => { + describe('clearFilter', () => { + it('emits clear event on filter removal', () => { + // arrange + const expectedChange = FilterChangeDetailsStub.forClear(); + let actualChange: FilterChangeDetails | undefined; + const context = new ContextBuilder().build(); + context.filterChanged.on((change) => { + actualChange = change; + }); + // act + context.clearFilter(); + // assert + expectExists(actualChange); + expect(actualChange).to.deep.equal(expectedChange); + }); + it('clears current filter', () => { + // arrange + const context = new ContextBuilder().build(); + // act + context.applyFilter('non-important'); + context.clearFilter(); + // assert + expect(context.currentFilter).to.be.equal(undefined); + }); + }); + describe('applyFilter', () => { + it('updates current filter correctly', () => { + // arrange + const expectedFilter = new FilterResultStub(); + const strategy = new FilterStrategyStub() + .withApplyFilterResult(expectedFilter); + const context = new ContextBuilder() + .withStrategy(strategy) + .build(); + // act + context.applyFilter('non-important'); + // assert + const actualFilter = context.currentFilter; + expect(actualFilter).to.equal(expectedFilter); + }); + it('emits apply event with correct filter', () => { + // arrange + const expectedFilter = new FilterResultStub(); + const strategy = new FilterStrategyStub() + .withApplyFilterResult(expectedFilter); + const context = new ContextBuilder() + .withStrategy(strategy) + .build(); + let actualFilter: FilterResult | undefined; + context.filterChanged.on((filterResult) => { + filterResult.visit({ + onApply: (result) => { + actualFilter = result; + }, + }); + }); + // act + context.applyFilter('non-important'); + // assert + expect(actualFilter).to.equal(expectedFilter); + }); + it('applies filter using current collection', () => { + // arrange + const expectedCollection = new CategoryCollectionStub(); + const strategy = new FilterStrategyStub(); + const context = new ContextBuilder() + .withStrategy(strategy) + .withCategoryCollection(expectedCollection) + .build(); + // act + context.applyFilter('non-important'); + // assert + const applyFilterCalls = strategy.callHistory.filter((c) => c.methodName === 'applyFilter'); + expect(applyFilterCalls).to.have.lengthOf(1); + const [, actualCollection] = applyFilterCalls[0].args; + expect(actualCollection).to.equal(expectedCollection); + }); + it('applies filter with given query', () => { + // arrange + const expectedQuery = 'expected-query'; + const strategy = new FilterStrategyStub(); + const context = new ContextBuilder() + .withStrategy(strategy) + .build(); + // act + context.applyFilter(expectedQuery); + // assert + const applyFilterCalls = strategy.callHistory.filter((c) => c.methodName === 'applyFilter'); + expect(applyFilterCalls).to.have.lengthOf(1); + const [actualQuery] = applyFilterCalls[0].args; + expect(actualQuery).to.equal(expectedQuery); + }); + }); +}); + +class ContextBuilder { + private categoryCollection: ICategoryCollection = new CategoryCollectionStub(); + + private filterStrategy: FilterStrategy = new FilterStrategyStub(); + + public build(): AdaptiveFilterContext { + return new AdaptiveFilterContext( + this.categoryCollection, + this.filterStrategy, + ); + } + + public withStrategy(strategy: FilterStrategy): this { + this.filterStrategy = strategy; + return this; + } + + public withCategoryCollection(categoryCollection: ICategoryCollection): this { + this.categoryCollection = categoryCollection; + return this; + } +} diff --git a/tests/unit/application/Context/State/Filter/Event/FilterChange.spec.ts b/tests/unit/application/Context/State/Filter/Event/FilterChange.spec.ts new file mode 100644 index 00000000..352ccbcd --- /dev/null +++ b/tests/unit/application/Context/State/Filter/Event/FilterChange.spec.ts @@ -0,0 +1,110 @@ +import { describe, it, expect } from 'vitest'; +import { FilterChange } from '@/application/Context/State/Filter/Event/FilterChange'; +import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub'; +import { FilterActionType } from '@/application/Context/State/Filter/Event/FilterActionType'; +import { FilterChangeDetailsVisitorStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsVisitorStub'; +import type { ApplyFilterAction } from '@/application/Context/State/Filter/Event/FilterChangeDetails'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; + +describe('FilterChange', () => { + describe('forApply', () => { + it('sets filter result', () => { + // arrange + const expectedFilter = new FilterResultStub(); + // act + const sut = FilterChange.forApply(expectedFilter); + // assert + const actualFilter = (sut.action as ApplyFilterAction).filter; + expect(actualFilter).to.equal(expectedFilter); + }); + it('sets action as expected', () => { + // arrange + const expectedAction = FilterActionType.Apply; + // act + const sut = FilterChange.forApply(new FilterResultStub()); + // assert + const actualAction = sut.action.type; + expect(actualAction).to.equal(expectedAction); + }); + }); + describe('forClear', () => { + it('does not set filter result', () => { + // arrange + const expectedFilter = undefined; + // act + const sut = FilterChange.forClear(); + // assert + const actualFilter = (sut.action as ApplyFilterAction).filter; + expect(actualFilter).to.equal(expectedFilter); + }); + it('sets action as expected', () => { + // arrange + const expectedAction = FilterActionType.Clear; + // act + const sut = FilterChange.forClear(); + // assert + const actualAction = sut.action.type; + expect(actualAction).to.equal(expectedAction); + }); + }); + describe('visit', () => { + describe('onClear', () => { + it('visits once', () => { + // arrange + const sut = FilterChange.forClear(); + const visitor = new FilterChangeDetailsVisitorStub(); + // act + sut.visit(visitor); + // assert + expect(visitor.callHistory).to.have.lengthOf(1); + }); + + it('visits onClear', () => { + // arrange + const sut = FilterChange.forClear(); + const visitor = new FilterChangeDetailsVisitorStub(); + // act + sut.visit(visitor); + // assert + const call = visitor.callHistory.find((c) => c.methodName === 'onClear'); + expect(call).toBeDefined(); + }); + }); + describe('onApply', () => { + it('visits once', () => { + // arrange + const sut = FilterChange.forApply(new FilterResultStub()); + const visitor = new FilterChangeDetailsVisitorStub(); + // act + sut.visit(visitor); + // assert + expect(visitor.callHistory).to.have.lengthOf(1); + }); + + it('visits onApply', () => { + // arrange + const sut = FilterChange.forApply(new FilterResultStub()); + const visitor = new FilterChangeDetailsVisitorStub(); + // act + sut.visit(visitor); + // assert + const call = visitor.callHistory.find((c) => c.methodName === 'onApply'); + expect(call).toBeDefined(); + }); + + it('visits with expected filter', () => { + // arrange + const expectedFilter = new FilterResultStub(); + const sut = FilterChange.forApply(expectedFilter); + const visitor = new FilterChangeDetailsVisitorStub(); + // act + sut.visit(visitor); + // assert + const call = visitor.callHistory.find((c) => c.methodName === 'onApply'); + expectExists(call); + const [actualFilter] = call.args; + expect(actualFilter).to.equal(expectedFilter); + }); + }); + }); +}); diff --git a/tests/unit/application/Context/State/Filter/Result/AppliedFilterResult.spec.ts b/tests/unit/application/Context/State/Filter/Result/AppliedFilterResult.spec.ts new file mode 100644 index 00000000..cea4b0b3 --- /dev/null +++ b/tests/unit/application/Context/State/Filter/Result/AppliedFilterResult.spec.ts @@ -0,0 +1,104 @@ +import { describe, it, expect } from 'vitest'; +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { AppliedFilterResult } from '@/application/Context/State/Filter/Result/AppliedFilterResult'; +import type { Category } from '@/domain/Executables/Category/Category'; +import type { Script } from '@/domain/Executables/Script/Script'; + +describe('AppliedFilterResult', () => { + describe('constructor', () => { + it('initializes query correctly', () => { + // arrange + const expectedQuery = 'expected query'; + const builder = new ResultBuilder() + .withQuery(expectedQuery); + // act + const result = builder.build(); + // assert + const actualQuery = result.query; + expect(actualQuery).to.equal(expectedQuery); + }); + }); + describe('hasAnyMatches', () => { + it('returns false with no matches', () => { + // arrange + const expected = false; + const result = new ResultBuilder() + .withScriptMatches([]) + .withCategoryMatches([]) + .build(); + // act + const actual = result.hasAnyMatches(); + // assert + expect(actual).to.equal(expected); + }); + it('returns true with script matches', () => { + // arrange + const expected = true; + const result = new ResultBuilder() + .withScriptMatches([new ScriptStub('matched-script')]) + .withCategoryMatches([]) + .build(); + // act + const actual = result.hasAnyMatches(); + expect(actual).to.equal(expected); + }); + it('returns true with category matches', () => { + // arrange + const expected = true; + const result = new ResultBuilder() + .withScriptMatches([]) + .withCategoryMatches([new CategoryStub('matched-category')]) + .build(); + // act + const actual = result.hasAnyMatches(); + expect(actual).to.equal(expected); + }); + it('returns true with script and category matches', () => { + // arrange + const expected = true; + const result = new ResultBuilder() + .withScriptMatches([new ScriptStub('matched-script')]) + .withCategoryMatches([new CategoryStub('matched-category')]) + .build(); + // act + const actual = result.hasAnyMatches(); + expect(actual).to.equal(expected); + }); + }); +}); + +class ResultBuilder { + private scriptMatches: readonly Script[] = [ + new ScriptStub(`[${ResultBuilder.name}]matched-script`), + ]; + + private categoryMatches: readonly Category[] = [ + new CategoryStub(`[${ResultBuilder.name}]matched-category`), + ]; + + private query: string = `[${ResultBuilder.name}]query`; + + public withScriptMatches(scriptMatches: readonly Script[]): this { + this.scriptMatches = scriptMatches; + return this; + } + + public withCategoryMatches(categoryMatches: readonly Category[]): this { + this.categoryMatches = categoryMatches; + return this; + } + + public withQuery(query: string) { + this.query = query; + return this; + } + + public build(): AppliedFilterResult { + return new AppliedFilterResult( + this.scriptMatches, + this.categoryMatches, + this.query, + ); + } +} diff --git a/tests/unit/application/Context/State/Filter/Strategy/LinearFilterStrategy.spec.ts b/tests/unit/application/Context/State/Filter/Strategy/LinearFilterStrategy.spec.ts new file mode 100644 index 00000000..9a3bcab0 --- /dev/null +++ b/tests/unit/application/Context/State/Filter/Strategy/LinearFilterStrategy.spec.ts @@ -0,0 +1,281 @@ +import { describe, it, expect } from 'vitest'; +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { Category } from '@/domain/Executables/Category/Category'; +import type { Script } from '@/domain/Executables/Script/Script'; +import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult'; +import { LinearFilterStrategy } from '@/application/Context/State/Filter/Strategy/LinearFilterStrategy'; + +describe('LinearFilterStrategy', () => { + describe('applyFilter', () => { + describe('query', () => { + it('returns provided filter', () => { + // arrange + const expectedQuery = 'non matching filter'; + const strategy = new FilterStrategyTestBuilder() + .withFilter(expectedQuery); + // act + const result = strategy.applyFilter(); + // assert + expect(result.query).to.equal(expectedQuery); + }); + }); + describe('hasAnyMatches', () => { + it('returns false when there are no matches', () => { + // arrange + const strategy = new FilterStrategyTestBuilder() + .withFilter('non matching filter') + .withCollection(new CategoryCollectionStub()); + // act + const result = strategy.applyFilter(); + // assert + expect(result.hasAnyMatches()).be.equal(false); + }); + it('returns true for a script match', () => { + // arrange + const matchingFilter = 'matching filter'; + const collection = new CategoryCollectionStub() + .withAction( + new CategoryStub('parent-category-of-matching-script') + .withScript(createMatchingScript(matchingFilter)), + ); + const strategy = new FilterStrategyTestBuilder() + .withFilter(matchingFilter) + .withCollection(collection); + // act + const result = strategy.applyFilter(); + // assert + expect(result.hasAnyMatches()).be.equal(true); + }); + it('returns true for a category match', () => { + // arrange + const matchingFilter = 'matching filter'; + const collection = new CategoryCollectionStub() + .withAction(createMatchingCategory(matchingFilter)); + const strategy = new FilterStrategyTestBuilder() + .withFilter(matchingFilter) + .withCollection(collection); + // act + const result = strategy.applyFilter(); + // assert + expect(result.hasAnyMatches()).be.equal(true); + }); + it('returns true for script and category matches', () => { + // arrange + const matchingFilter = 'matching filter'; + const collection = new CategoryCollectionStub() + .withAction(createMatchingCategory(matchingFilter)) + .withAction(new CategoryStub('matching-script-parent').withScript(createMatchingScript(matchingFilter))); + const strategy = new FilterStrategyTestBuilder() + .withFilter(matchingFilter) + .withCollection(collection); + // act + const result = strategy.applyFilter(); + // assert + expect(result.hasAnyMatches()).be.equal(true); + }); + }); + describe('scriptMatches', () => { + it('returns empty when there are no matches', () => { + // arrange + const strategy = new FilterStrategyTestBuilder() + .withFilter('non matching filter') + .withCollection(new CategoryCollectionStub()); + // act + const result = strategy.applyFilter(); + // assert + expect(result.scriptMatches).to.have.lengthOf(0); + }); + describe('returns single matching script', () => { + interface ScriptMatchTestScenario { + readonly description: string; + readonly filter: string; + readonly matchingScript: Script; + } + const testScenarios: readonly ScriptMatchTestScenario[] = [ + { + description: 'case-insensitive code', + filter: 'Hello WoRLD', + matchingScript: new ScriptStub('id').withCode('HELLO world'), + }, + { + description: 'case-insensitive revert code', + filter: 'Hello WoRLD', + matchingScript: new ScriptStub('id').withRevertCode('HELLO world'), + }, + { + description: 'case-insensitive name', + filter: 'Hello WoRLD', + matchingScript: new ScriptStub('id').withName('HELLO world'), + }, + { + description: 'case-insensitive documentation', + filter: 'MaTChing doC', + matchingScript: new ScriptStub('id').withDocs(['unrelated docs', 'matching Docs']), + }, + ]; + testScenarios.forEach(({ + description, filter, matchingScript, + }) => { + it(description, () => { + // arrange + const expectedMatches = [matchingScript]; + const collection = new CategoryCollectionStub() + .withAction(new CategoryStub('matching-script-parent').withScript(matchingScript)); + const strategy = new FilterStrategyTestBuilder() + .withFilter(filter) + .withCollection(collection); + // act + const result = strategy.applyFilter(); + // assert + expectScriptMatches(result, expectedMatches); + }); + }); + }); + it('returns multiple matching scripts', () => { + // arrange + const filter = 'matching filter'; + const matchingScripts: readonly Script[] = [ + createMatchingScript(filter), + createMatchingScript(filter), + ]; + const expectedMatches = [...matchingScripts]; + const collection = new CategoryCollectionStub() + .withAction(new CategoryStub('matching-scripts-parent').withScripts(...matchingScripts)); + const strategy = new FilterStrategyTestBuilder() + .withFilter(filter) + .withCollection(collection); + // act + const result = strategy.applyFilter(); + // assert + expectScriptMatches(result, expectedMatches); + }); + }); + describe('categoryMatches', () => { + it('returns empty when there are no matches', () => { + // arrange + const strategy = new FilterStrategyTestBuilder() + .withFilter('non matching filter') + .withCollection(new CategoryCollectionStub()); + // act + const result = strategy.applyFilter(); + // assert + expect(result.categoryMatches).to.have.lengthOf(0); + }); + describe('returns single matching category', () => { + interface CategoryMatchTestScenario { + readonly description: string; + readonly filter: string; + readonly matchingCategory: Category; + } + const testScenarios: readonly CategoryMatchTestScenario[] = [ + { + description: 'match with case-insensitive name', + filter: 'Hello WoRLD', + matchingCategory: new CategoryStub('matching-script-parent').withName('HELLO world'), + }, + { + description: 'case-sensitive documentation', + filter: 'Hello WoRLD', + matchingCategory: new CategoryStub('matching-script-parent').withDocs(['unrelated-docs', 'HELLO world']), + }, + ]; + testScenarios.forEach(({ + description, filter, matchingCategory, + }) => { + it(description, () => { + // arrange + const expectedMatches = [matchingCategory]; + const collection = new CategoryCollectionStub() + .withAction(matchingCategory); + const strategy = new FilterStrategyTestBuilder() + .withFilter(filter) + .withCollection(collection); + // act + const result = strategy.applyFilter(); + // assert + expectCategoryMatches(result, expectedMatches); + }); + }); + }); + it('returns multiple matching categories', () => { + // arrange + const filter = 'matching filter'; + const matchingCategories: readonly Category[] = [ + createMatchingCategory(filter), + createMatchingCategory(filter), + ]; + const expectedMatches = [...matchingCategories]; + const collection = new CategoryCollectionStub() + .withActions(...matchingCategories); + const strategy = new FilterStrategyTestBuilder() + .withFilter(filter) + .withCollection(collection); + // act + const result = strategy.applyFilter(); + // assert + expectCategoryMatches(result, expectedMatches); + }); + }); + }); +}); + +function createMatchingScript( + matchingFilter: string, +): ScriptStub { + return new ScriptStub('matching-script') + .withCode(matchingFilter) + .withName(matchingFilter); +} + +function createMatchingCategory( + matchingFilter: string, +): CategoryStub { + return new CategoryStub('matching-category') + .withName(matchingFilter) + .withDocs([matchingFilter]); +} + +function expectCategoryMatches( + actualFilter: FilterResult, + expectedMatches: readonly Category[], +): void { + expect(actualFilter.hasAnyMatches()).be.equal(true); + expect(actualFilter.categoryMatches).to.have.lengthOf(expectedMatches.length); + expect(actualFilter.categoryMatches).to.have.members(expectedMatches); +} + +function expectScriptMatches( + actualFilter: FilterResult, + expectedMatches: readonly Script[], +): void { + expect(actualFilter.hasAnyMatches()).be.equal(true); + expect(actualFilter.scriptMatches).to.have.lengthOf(expectedMatches.length); + expect(actualFilter.scriptMatches).to.have.members(expectedMatches); +} + +class FilterStrategyTestBuilder { + private filter: string = `[${FilterStrategyTestBuilder.name}]filter`; + + private collection: ICategoryCollection = new CategoryCollectionStub(); + + public withCollection(collection: ICategoryCollection): this { + this.collection = collection; + return this; + } + + public withFilter(filter: string): this { + this.filter = filter; + return this; + } + + public applyFilter(): ReturnType { + const strategy = new LinearFilterStrategy(); + return strategy.applyFilter( + this.filter, + this.collection, + ); + } +} diff --git a/tests/unit/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.spec.ts b/tests/unit/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.spec.ts new file mode 100644 index 00000000..c2c9f1ec --- /dev/null +++ b/tests/unit/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.spec.ts @@ -0,0 +1,272 @@ +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import type { ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import { ScriptToCategorySelectionMapper } from '@/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper'; +import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub'; +import type { CategorySelectionChange } from '@/application/Context/State/Selection/Category/CategorySelectionChange'; +import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; + +describe('ScriptToCategorySelectionMapper', () => { + describe('areAllScriptsSelected', () => { + it('should return false for partially selected scripts', () => { + // arrange + const expected = false; + const { sut, category } = setupTestWithPreselectedScripts({ + preselect: (allScripts) => [allScripts[0]], + }); + // act + const actual = sut.areAllScriptsSelected(category); + // assert + expect(actual).to.equal(expected); + }); + it('should return true when all scripts are selected', () => { + // arrange + const expected = true; + const { sut, category } = setupTestWithPreselectedScripts({ + preselect: (allScripts) => [...allScripts], + }); + // act + const actual = sut.areAllScriptsSelected(category); + // assert + expect(actual).to.equal(expected); + }); + }); + describe('isAnyScriptSelected', () => { + it('should return false with no selected scripts', () => { + // arrange + const expected = false; + const { sut, category } = setupTestWithPreselectedScripts({ + preselect: () => [], + }); + // act + const actual = sut.isAnyScriptSelected(category); + // assert + expect(actual).to.equal(expected); + }); + it('should return true with at least one script selected', () => { + // arrange + const expected = true; + const { sut, category } = setupTestWithPreselectedScripts({ + preselect: (allScripts) => [allScripts[0]], + }); + // act + const actual = sut.isAnyScriptSelected(category); + // assert + expect(actual).to.equal(expected); + }); + }); + describe('processChanges', () => { + const testScenarios: ReadonlyArray<{ + readonly description: string; + readonly changes: readonly CategorySelectionChange[]; + readonly categories: ReadonlyArray<{ + readonly categoryId: ExecutableId, + readonly scriptIds: readonly ExecutableId[], + }>; + readonly expected: readonly ScriptSelectionChange[], + }> = [ + { + description: 'single script: select without revert', + categories: [ + { categoryId: 'category-1', scriptIds: ['single-script'] }, + ], + changes: [ + { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } }, + ], + expected: [ + { scriptId: 'single-script', newStatus: { isSelected: true, isReverted: false } }, + ], + }, + { + description: 'multiple scripts: select without revert', + categories: [ + { categoryId: 'category-1', scriptIds: ['script1-cat1', 'script2-cat1'] }, + { categoryId: 'category-2', scriptIds: ['script3-cat2'] }, + ], + changes: [ + { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } }, + { categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } }, + ], + expected: [ + { scriptId: 'script1-cat1', newStatus: { isSelected: true, isReverted: false } }, + { scriptId: 'script2-cat1', newStatus: { isSelected: true, isReverted: false } }, + { scriptId: 'script3-cat2', newStatus: { isSelected: true, isReverted: false } }, + ], + }, + { + description: 'single script: select with revert', + categories: [ + { categoryId: 'category-1', scriptIds: ['single-script'] }, + ], + changes: [ + { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } }, + ], + expected: [ + { scriptId: 'single-script', newStatus: { isSelected: true, isReverted: true } }, + ], + }, + { + description: 'multiple scripts: select with revert', + categories: [ + { categoryId: 'category-1', scriptIds: ['script-1-cat-1'] }, + { categoryId: 'category-2', scriptIds: ['script-2-cat-2'] }, + { categoryId: 'category-3', scriptIds: ['script-3-cat-3'] }, + ], + changes: [ + { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } }, + { categoryId: 'category-2', newStatus: { isSelected: true, isReverted: true } }, + { categoryId: 'category-3', newStatus: { isSelected: true, isReverted: true } }, + ], + expected: [ + { scriptId: 'script-1-cat-1', newStatus: { isSelected: true, isReverted: true } }, + { scriptId: 'script-2-cat-2', newStatus: { isSelected: true, isReverted: true } }, + { scriptId: 'script-3-cat-3', newStatus: { isSelected: true, isReverted: true } }, + ], + }, + { + description: 'single script: deselect', + categories: [ + { categoryId: 'category-1', scriptIds: ['single-script'] }, + ], + changes: [ + { categoryId: 'category-1', newStatus: { isSelected: false } }, + ], + expected: [ + { scriptId: 'single-script', newStatus: { isSelected: false } }, + ], + }, + { + description: 'multiple scripts: deselect', + categories: [ + { categoryId: 'category-1', scriptIds: ['script-1-cat1'] }, + { categoryId: 'category-2', scriptIds: ['script-2-cat2'] }, + ], + changes: [ + { categoryId: 'category-1', newStatus: { isSelected: false } }, + { categoryId: 'category-2', newStatus: { isSelected: false } }, + ], + expected: [ + { scriptId: 'script-1-cat1', newStatus: { isSelected: false } }, + { scriptId: 'script-2-cat2', newStatus: { isSelected: false } }, + ], + }, + { + description: 'mixed operations (select, revert, deselect)', + categories: [ + { categoryId: 'category-1', scriptIds: ['to-revert'] }, + { categoryId: 'category-2', scriptIds: ['not-revert'] }, + { categoryId: 'category-3', scriptIds: ['to-deselect'] }, + ], + changes: [ + { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } }, + { categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } }, + { categoryId: 'category-3', newStatus: { isSelected: false } }, + ], + expected: [ + { scriptId: 'to-revert', newStatus: { isSelected: true, isReverted: true } }, + { scriptId: 'not-revert', newStatus: { isSelected: true, isReverted: false } }, + { scriptId: 'to-deselect', newStatus: { isSelected: false } }, + ], + }, + { + description: 'affecting selected categories only', + categories: [ + { categoryId: 'category-1', scriptIds: ['relevant-1', 'relevant-2'] }, + { categoryId: 'category-2', scriptIds: ['not-relevant-1', 'not-relevant-2'] }, + { categoryId: 'category-3', scriptIds: ['not-relevant-3', 'not-relevant-4'] }, + ], + changes: [ + { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } }, + ], + expected: [ + { scriptId: 'relevant-1', newStatus: { isSelected: true, isReverted: true } }, + { scriptId: 'relevant-2', newStatus: { isSelected: true, isReverted: true } }, + ], + }, + ]; + testScenarios.forEach(({ + description, changes, categories, expected, + }) => { + it(description, () => { + // arrange + const scriptSelectionStub = new ScriptSelectionStub(); + const sut = new ScriptToCategorySelectionMapperBuilder() + .withScriptSelection(scriptSelectionStub) + .withCollection(new CategoryCollectionStub().withAction( + new CategoryStub('single-parent-category-action') + // Register scripts to test for nested items + .withAllScriptIdsRecursively(...categories.flatMap((c) => c.scriptIds)) + .withCategories(...categories.map( + (c) => new CategoryStub(c.categoryId).withAllScriptIdsRecursively(...c.scriptIds), + )), + )) + .build(); + // act + sut.processChanges({ + changes, + }); + // assert + expect(scriptSelectionStub.callHistory).to.have.lengthOf(1); + const call = scriptSelectionStub.callHistory.find((m) => m.methodName === 'processChanges'); + expectExists(call); + const [command] = call.args; + const { changes: actualChanges } = (command as ScriptSelectionChangeCommand); + expect(actualChanges).to.have.lengthOf(expected.length); + expect(actualChanges).to.deep.members(expected); + }); + }); + }); +}); + +class ScriptToCategorySelectionMapperBuilder { + private scriptSelection: ScriptSelection = new ScriptSelectionStub(); + + private collection: ICategoryCollection = new CategoryCollectionStub(); + + public withScriptSelection(scriptSelection: ScriptSelection): this { + this.scriptSelection = scriptSelection; + return this; + } + + public withCollection(collection: ICategoryCollection): this { + this.collection = collection; + return this; + } + + public build(): ScriptToCategorySelectionMapper { + return new ScriptToCategorySelectionMapper( + this.scriptSelection, + this.collection, + ); + } +} + +type TestScripts = readonly [ScriptStub, ScriptStub, ScriptStub]; +function setupTestWithPreselectedScripts(options: { + preselect: (allScripts: TestScripts) => readonly ScriptStub[], +}) { + const allScripts: TestScripts = [ + new ScriptStub('first-script'), + new ScriptStub('second-script'), + new ScriptStub('third-script'), + ]; + const preselectedScripts = options.preselect(allScripts); + const category = new CategoryStub('single-parent-category-action') + .withAllScriptsRecursively(...allScripts); // Register scripts to test for nested items + const collection = new CategoryCollectionStub().withAction(category); + const sut = new ScriptToCategorySelectionMapperBuilder() + .withCollection(collection) + .withScriptSelection( + new ScriptSelectionStub() + .withSelectedScripts(preselectedScripts.map((s) => s.toSelectedScript())), + ) + .build(); + return { + category, + sut, + }; +} diff --git a/tests/unit/application/Context/State/Selection/Script/DebouncedScriptSelection.spec.ts b/tests/unit/application/Context/State/Selection/Script/DebouncedScriptSelection.spec.ts new file mode 100644 index 00000000..b0ef2cdc --- /dev/null +++ b/tests/unit/application/Context/State/Selection/Script/DebouncedScriptSelection.spec.ts @@ -0,0 +1,658 @@ +import { describe, it, expect } from 'vitest'; +import { type DebounceFunction, DebouncedScriptSelection } from '@/application/Context/State/Selection/Script/DebouncedScriptSelection'; +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; +import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import { BatchedDebounceStub } from '@tests/unit/shared/Stubs/BatchedDebounceStub'; +import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import type { Script } from '@/domain/Executables/Script/Script'; +import { expectEqualSelectedScripts } from './ExpectEqualSelectedScripts'; + +type DebounceArg = ScriptSelectionChangeCommand; + +describe('DebouncedScriptSelection', () => { + describe('constructor', () => { + describe('initialization of selected scripts', () => { + const testScenarios: ReadonlyArray<{ + readonly description: string; + readonly selectedScripts: readonly SelectedScript[]; + }> = [ + { + description: 'initializes with no scripts when given empty array', + selectedScripts: [], + }, + { + description: 'initializes with a single script when given one script', + selectedScripts: [new SelectedScriptStub(new ScriptStub('s1'))], + }, + { + description: 'initializes with multiple scripts when given multiple scripts', + selectedScripts: [ + new SelectedScriptStub(new ScriptStub('s1')), + new SelectedScriptStub(new ScriptStub('s2')), + ], + }, + ]; + testScenarios.forEach(({ description, selectedScripts }) => { + it(description, () => { + // arrange + const expectedScripts = selectedScripts; + const builder = new DebouncedScriptSelectionBuilder() + .withSelectedScripts(selectedScripts); + // act + const selection = builder.build(); + const actualScripts = selection.selectedScripts; + // assert + expectEqualSelectedScripts(actualScripts, expectedScripts); + }); + }); + }); + describe('debounce configuration', () => { + /* + Note: These tests cover internal implementation details, particularly the debouncing logic, + to ensure comprehensive code coverage. They are not focused on the public API. While useful + for detecting subtle bugs, they might need updates during refactoring if internal structures + change but external behaviors remain the same. + */ + it('sets up debounce with a callback function', () => { + // arrange + const debounceStub = new BatchedDebounceStub(); + const builder = new DebouncedScriptSelectionBuilder() + .withBatchedDebounce(debounceStub.func); + // act + builder.build(); + // assert + expect(debounceStub.callHistory).to.have.lengthOf(1); + const [debounceFunc] = debounceStub.callHistory[0]; + expectExists(debounceFunc); + }); + it('configures debounce with specific delay ', () => { + // arrange + const expectedDebounceInMs = 100; + const debounceStub = new BatchedDebounceStub(); + const builder = new DebouncedScriptSelectionBuilder() + .withBatchedDebounce(debounceStub.func); + // act + builder.build(); + // assert + expect(debounceStub.callHistory).to.have.lengthOf(1); + const [, waitInMs] = debounceStub.callHistory[0]; + expect(waitInMs).to.equal(expectedDebounceInMs); + }); + it('applies debouncing to processChanges method', () => { + // arrange + const expectedFunc = () => {}; + const debounceMock: DebounceFunction = () => expectedFunc; + const builder = new DebouncedScriptSelectionBuilder() + .withBatchedDebounce(debounceMock); + // act + const selection = builder.build(); + // assert + const actualFunction = selection.processChanges; + expect(actualFunction).to.equal(expectedFunc); + }); + }); + }); + describe('isSelected', () => { + it('returns false for an unselected script', () => { + // arrange + const expectedResult = false; + const { scriptSelection, unselectedScripts } = setupTestWithPreselectedScripts({ + preselect: (allScripts) => [allScripts[0]], + }); + const scriptIdToCheck = unselectedScripts[0].executableId; + // act + const actual = scriptSelection.isSelected(scriptIdToCheck); + // assert + expect(actual).to.equal(expectedResult); + }); + it('returns true for a selected script', () => { + // arrange + const expectedResult = true; + const { scriptSelection, preselectedScripts } = setupTestWithPreselectedScripts({ + preselect: (allScripts) => [allScripts[0]], + }); + const scriptIdToCheck = preselectedScripts[0].id; + // act + const actual = scriptSelection.isSelected(scriptIdToCheck); + // assert + expect(actual).to.equal(expectedResult); + }); + }); + describe('deselectAll', () => { + it('removes all selected scripts', () => { + // arrange + const { scriptSelection, changeEvents } = setupTestWithPreselectedScripts({ + preselect: (scripts) => [scripts[0], scripts[1]], + }); + // act + scriptSelection.deselectAll(); + // assert + expect(changeEvents).to.have.lengthOf(1); + expect(changeEvents[0]).to.have.lengthOf(0); + expect(scriptSelection.selectedScripts).to.have.lengthOf(0); + }); + it('does not notify when no scripts are selected', () => { + // arrange + const { scriptSelection, changeEvents } = setupTestWithPreselectedScripts({ + preselect: () => [], + }); + // act + scriptSelection.deselectAll(); + // assert + expect(changeEvents).to.have.lengthOf(0); + expect(scriptSelection.selectedScripts).to.have.lengthOf(0); + }); + }); + describe('selectAll', () => { + it('selects all available scripts', () => { + // arrange + const selectedRevertState = false; + const { scriptSelection, changeEvents, allScripts } = setupTestWithPreselectedScripts({ + preselect: () => [], + }); + const expectedSelection = allScripts.map( + (s) => s.toSelectedScript().withRevert(selectedRevertState), + ); + // act + scriptSelection.selectAll(); + // assert + expect(changeEvents).to.have.lengthOf(1); + expectEqualSelectedScripts(changeEvents[0], expectedSelection); + expectEqualSelectedScripts(scriptSelection.selectedScripts, expectedSelection); + }); + it('does not notify when no new scripts are selected', () => { + // arrange + const { scriptSelection, changeEvents } = setupTestWithPreselectedScripts({ + preselect: (allScripts) => allScripts, + }); + // act + scriptSelection.selectAll(); + // assert + expect(changeEvents).to.have.lengthOf(0); + }); + }); + describe('selectOnly', () => { + describe('selects correctly', () => { + const testScenarios: ReadonlyArray<{ + readonly description: string; + readonly preselect: (allScripts: TestScripts) => readonly SelectedScriptStub[], + readonly toSelect: (allScripts: TestScripts) => readonly ScriptStub[]; + readonly getExpectedFinalSelection: (allScripts: TestScripts) => readonly SelectedScript[], + }> = [ + { + description: 'adds expected scripts to empty selection as non-reverted', + preselect: () => [], + toSelect: (allScripts) => [allScripts[0]], + getExpectedFinalSelection: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(false), + ], + }, + { + description: 'adds expected scripts to existing selection as non-reverted', + preselect: (allScripts) => [allScripts[0], allScripts[1]] + .map((s) => s.toSelectedScript().withRevert(false)), + toSelect: (allScripts) => [...allScripts], + getExpectedFinalSelection: (allScripts) => allScripts + .map((s) => s.toSelectedScript().withRevert(false)), + }, + { + description: 'removes other scripts from selection', + preselect: (allScripts) => allScripts + .map((s) => s.toSelectedScript().withRevert(false)), + toSelect: (allScripts) => [allScripts[0]], + getExpectedFinalSelection: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(false), + ], + }, + { + description: 'handles both addition and removal of scripts correctly', + preselect: (allScripts) => [allScripts[0], allScripts[2]] // Removes "2" + .map((s) => s.toSelectedScript().withRevert(false)), + toSelect: (allScripts) => [allScripts[0], allScripts[1]], // Adds "1" + getExpectedFinalSelection: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(false), + allScripts[1].toSelectedScript().withRevert(false), + ], + }, + ]; + testScenarios.forEach(({ + description, preselect, toSelect, getExpectedFinalSelection, + }) => { + it(description, () => { + // arrange + const { scriptSelection, changeEvents, allScripts } = setupTestWithPreselectedScripts({ + preselect, + }); + const scriptsToSelect = toSelect(allScripts); + const expectedSelection = getExpectedFinalSelection(allScripts); + // act + scriptSelection.selectOnly(scriptsToSelect); + // assert + expect(changeEvents).to.have.lengthOf(1); + expectEqualSelectedScripts(changeEvents[0], expectedSelection); + expectEqualSelectedScripts(scriptSelection.selectedScripts, expectedSelection); + }); + }); + }); + describe('does not notify for unchanged selection', () => { + const testScenarios: ReadonlyArray<{ + readonly description: string; + readonly preselect: TestScriptSelector; + }> = [ + { + description: 'unchanged selection with reverted scripts', + preselect: (allScripts) => allScripts.map((s) => s.toSelectedScript().withRevert(true)), + }, + { + description: 'unchanged selection with non-reverted scripts', + preselect: (allScripts) => allScripts.map((s) => s.toSelectedScript().withRevert(false)), + }, + { + description: 'unchanged selection with mixed revert states', + preselect: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(true), + allScripts[1].toSelectedScript().withRevert(false), + ], + }, + ]; + testScenarios.forEach(({ + description, preselect, + }) => { + it(description, () => { + // arrange + const { + scriptSelection, changeEvents, preselectedScripts, + } = setupTestWithPreselectedScripts({ preselect }); + const scriptsToSelect = preselectedScripts.map((s) => s.script); + // act + scriptSelection.selectOnly(scriptsToSelect); + // assert + expect(changeEvents).to.have.lengthOf(0); + }); + }); + }); + it('throws error when an empty script array is passed', () => { + // arrange + const expectedError = 'Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.'; + const scripts = []; + const scriptSelection = new DebouncedScriptSelectionBuilder().build(); + // act + const act = () => scriptSelection.selectOnly(scripts); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('processChanges', () => { + describe('mutates correctly', () => { + const testScenarios: ReadonlyArray<{ + readonly description: string; + readonly preselect: TestScriptSelector; + readonly getChanges: (allScripts: TestScripts) => readonly ScriptSelectionChange[]; + readonly getExpectedFinalSelection: (allScripts: TestScripts) => readonly SelectedScript[], + }> = [ + { + description: 'correctly adds a new reverted script', + preselect: (allScripts) => [allScripts[0], allScripts[1]] + .map((s) => s.toSelectedScript()), + getChanges: (allScripts) => [ + { + scriptId: allScripts[2].executableId, + newStatus: { isReverted: true, isSelected: true }, + }, + ], + getExpectedFinalSelection: (allScripts) => [ + allScripts[0].toSelectedScript(), + allScripts[1].toSelectedScript(), + new SelectedScriptStub(allScripts[2]).withRevert(true), + ], + }, + { + description: 'correctly adds a new non-reverted script', + preselect: (allScripts) => [allScripts[0], allScripts[1]] + .map((s) => s.toSelectedScript()), + getChanges: (allScripts) => [ + { + scriptId: allScripts[2].executableId, + newStatus: { isReverted: false, isSelected: true }, + }, + ], + getExpectedFinalSelection: (allScripts) => [ + allScripts[0].toSelectedScript(), + allScripts[1].toSelectedScript(), + new SelectedScriptStub(allScripts[2]).withRevert(false), + ], + }, + { + description: 'correctly removes an existing script', + preselect: (allScripts) => [allScripts[0], allScripts[1]] + .map((s) => s.toSelectedScript()), + getChanges: (allScripts) => [ + { scriptId: allScripts[0].executableId, newStatus: { isSelected: false } }, + ], + getExpectedFinalSelection: (allScripts) => [ + allScripts[1].toSelectedScript(), + ], + }, + { + description: 'updates revert status to true for an existing script', + preselect: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(false), + allScripts[1].toSelectedScript(), + ], + getChanges: (allScripts) => [ + { + scriptId: allScripts[0].executableId, + newStatus: { isSelected: true, isReverted: true }, + }, + ], + getExpectedFinalSelection: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(true), + allScripts[1].toSelectedScript(), + ], + }, + { + description: 'updates revert status to false for an existing script', + preselect: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(true), + allScripts[1].toSelectedScript(), + ], + getChanges: (allScripts) => [ + { + scriptId: allScripts[0].executableId, + newStatus: { isSelected: true, isReverted: false }, + }, + ], + getExpectedFinalSelection: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(false), + allScripts[1].toSelectedScript(), + ], + }, + { + description: 'handles mixed operations: add, update, remove', + preselect: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(true), // update + allScripts[2].toSelectedScript(), // remove + ], + getChanges: (allScripts) => [ + { + scriptId: allScripts[0].executableId, + newStatus: { isSelected: true, isReverted: false }, + }, + { + scriptId: allScripts[1].executableId, + newStatus: { isSelected: true, isReverted: true }, + }, + { + scriptId: allScripts[2].executableId, + newStatus: { isSelected: false }, + }, + ], + getExpectedFinalSelection: (allScripts) => [ + allScripts[0].toSelectedScript().withRevert(false), + allScripts[1].toSelectedScript().withRevert(true), + ], + }, + ]; + testScenarios.forEach(({ + description, preselect, getChanges, getExpectedFinalSelection, + }) => { + it(description, () => { + // arrange + const { scriptSelection, changeEvents, allScripts } = setupTestWithPreselectedScripts({ + preselect, + }); + const changes = getChanges(allScripts); + // act + scriptSelection.processChanges({ + changes, + }); + // assert + const expectedSelection = getExpectedFinalSelection(allScripts); + expect(changeEvents).to.have.lengthOf(1); + expectEqualSelectedScripts(changeEvents[0], expectedSelection); + expectEqualSelectedScripts(scriptSelection.selectedScripts, expectedSelection); + }); + }); + }); + describe('does not mutate for unchanged data', () => { + const testScenarios: ReadonlyArray<{ + readonly description: string; + readonly preselect: TestScriptSelector; + readonly getChanges: (allScripts: TestScripts) => readonly ScriptSelectionChange[]; + }> = [ + { + description: 'does not change selection for an already selected script', + preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(true)], + getChanges: (allScripts) => [ + { + scriptId: allScripts[0].executableId, + newStatus: { isReverted: true, isSelected: true }, + }, + ], + }, + { + description: 'does not change selection when deselecting a missing script', + preselect: (allScripts) => [allScripts[0], allScripts[1]] + .map((s) => s.toSelectedScript()), + getChanges: (allScripts) => [ + { scriptId: allScripts[2].executableId, newStatus: { isSelected: false } }, + ], + }, + { + description: 'handles no mutations for mixed unchanged operations', + preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(false)], + getChanges: (allScripts) => [ + { + scriptId: allScripts[0].executableId, + newStatus: { isSelected: true, isReverted: false }, + }, + { + scriptId: allScripts[1].executableId, + newStatus: { isSelected: false }, + }, + ], + }, + ]; + testScenarios.forEach(({ + description, preselect, getChanges, + }) => { + it(description, () => { + // arrange + const { scriptSelection, changeEvents, allScripts } = setupTestWithPreselectedScripts({ + preselect, + }); + const initialSelection = [...scriptSelection.selectedScripts]; + const changes = getChanges(allScripts); + // act + scriptSelection.processChanges({ + changes, + }); + // assert + expect(changeEvents).to.have.lengthOf(0); + expectEqualSelectedScripts(scriptSelection.selectedScripts, initialSelection); + }); + }); + }); + describe('debouncing', () => { + it('queues commands for debouncing', () => { + // arrange + const debounceStub = new BatchedDebounceStub(); + const script = new ScriptStub('test'); + const selection = new DebouncedScriptSelectionBuilder() + .withBatchedDebounce(debounceStub.func) + .withCollection(createCollectionWithScripts(script)) + .build(); + const expectedCommand: ScriptSelectionChangeCommand = { + changes: [ + { scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } }, + ], + }; + // act + selection.processChanges(expectedCommand); + // assert + expect(debounceStub.collectedArgs).to.have.lengthOf(1); + expect(debounceStub.collectedArgs[0]).to.equal(expectedCommand); + }); + it('does not apply changes during debouncing period', () => { + // arrange + const debounceStub = new BatchedDebounceStub() + .withImmediateDebouncing(false); + const script = new ScriptStub('test'); + const selection = new DebouncedScriptSelectionBuilder() + .withBatchedDebounce(debounceStub.func) + .withCollection(createCollectionWithScripts(script)) + .build(); + const changeEvents = watchForChangeEvents(selection); + // act + selection.processChanges({ + changes: [ + { scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } }, + ], + }); + // assert + expect(changeEvents).to.have.lengthOf(0); + expectEqualSelectedScripts(selection.selectedScripts, []); + }); + it('applies single change after debouncing period', () => { + // arrange + const debounceStub = new BatchedDebounceStub() + .withImmediateDebouncing(false); + const script = new ScriptStub('test'); + const selection = new DebouncedScriptSelectionBuilder() + .withBatchedDebounce(debounceStub.func) + .withCollection(createCollectionWithScripts(script)) + .build(); + const changeEvents = watchForChangeEvents(selection); + const expectedSelection = [script.toSelectedScript().withRevert(true)]; + // act + selection.processChanges({ + changes: [ + { scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } }, + ], + }); + debounceStub.execute(); + // assert + expect(changeEvents).to.have.lengthOf(1); + expectEqualSelectedScripts(selection.selectedScripts, expectedSelection); + }); + it('applies multiple changes after debouncing period', () => { + // arrange + const debounceStub = new BatchedDebounceStub() + .withImmediateDebouncing(false); + const scripts = [new ScriptStub('first'), new ScriptStub('second'), new ScriptStub('third')]; + const selection = new DebouncedScriptSelectionBuilder() + .withBatchedDebounce(debounceStub.func) + .withCollection(createCollectionWithScripts(...scripts)) + .build(); + const changeEvents = watchForChangeEvents(selection); + const expectedSelection = scripts.map((s) => s.toSelectedScript().withRevert(true)); + // act + for (const script of scripts) { + selection.processChanges({ + changes: [ + { scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } }, + ], + }); + } + debounceStub.execute(); + // assert + expect(changeEvents).to.have.lengthOf(1); + expectEqualSelectedScripts(selection.selectedScripts, expectedSelection); + }); + }); + }); +}); + +function createCollectionWithScripts(...scripts: Script[]): CategoryCollectionStub { + const category = new CategoryStub('parent-category').withScripts(...scripts); + const collection = new CategoryCollectionStub().withAction(category); + return collection; +} + +function watchForChangeEvents( + selection: DebouncedScriptSelection, +): ReadonlyArray { + const changes: Array = []; + selection.changed.on((s) => changes.push(s)); + return changes; +} + +type TestScripts = readonly [ScriptStub, ScriptStub, ScriptStub]; +type TestScriptSelector = ( + allScripts: TestScripts, +) => readonly SelectedScriptStub[] | readonly ScriptStub[]; +function setupTestWithPreselectedScripts(options: { + preselect: TestScriptSelector, +}) { + const allScripts: TestScripts = [ + new ScriptStub('first-script'), + new ScriptStub('second-script'), + new ScriptStub('third-script'), + ]; + const preselectedScripts = (() => { + const initialSelection = options.preselect(allScripts); + if (isScriptStubArray(initialSelection)) { + return initialSelection.map((s) => s.toSelectedScript().withRevert(false)); + } + return initialSelection; + })(); + const unselectedScripts = allScripts.filter( + (s) => !preselectedScripts.map((selected) => selected.id).includes(s.executableId), + ); + const collection = createCollectionWithScripts(...allScripts); + const scriptSelection = new DebouncedScriptSelectionBuilder() + .withSelectedScripts(preselectedScripts) + .withCollection(collection) + .build(); + const changeEvents = watchForChangeEvents(scriptSelection); + return { + allScripts, + unselectedScripts, + preselectedScripts, + scriptSelection, + changeEvents, + }; +} + +function isScriptStubArray(obj: readonly unknown[]): obj is readonly ScriptStub[] { + return obj.length > 0 && obj[0] instanceof ScriptStub; +} + +class DebouncedScriptSelectionBuilder { + private collection: ICategoryCollection = new CategoryCollectionStub() + .withSomeActions(); + + private selectedScripts: readonly SelectedScript[] = []; + + private batchedDebounce: DebounceFunction = new BatchedDebounceStub() + .withImmediateDebouncing(true) + .func; + + public withSelectedScripts(selectedScripts: readonly SelectedScript[]) { + this.selectedScripts = selectedScripts; + return this; + } + + public withBatchedDebounce(batchedDebounce: DebounceFunction) { + this.batchedDebounce = batchedDebounce; + return this; + } + + public withCollection(collection: ICategoryCollection) { + this.collection = collection; + return this; + } + + public build(): DebouncedScriptSelection { + return new DebouncedScriptSelection( + this.collection, + this.selectedScripts, + this.batchedDebounce, + ); + } +} diff --git a/tests/unit/application/Context/State/Selection/Script/ExpectEqualSelectedScripts.ts b/tests/unit/application/Context/State/Selection/Script/ExpectEqualSelectedScripts.ts new file mode 100644 index 00000000..5db7ffad --- /dev/null +++ b/tests/unit/application/Context/State/Selection/Script/ExpectEqualSelectedScripts.ts @@ -0,0 +1,48 @@ +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import { indentText } from '@/application/Common/Text/IndentText'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; + +export function expectEqualSelectedScripts( + actual: readonly SelectedScript[], + expected: readonly SelectedScript[], +) { + expectSameScriptIds(actual, expected); + expectSameRevertStates(actual, expected); +} + +function expectSameScriptIds( + actual: readonly SelectedScript[], + expected: readonly SelectedScript[], +) { + const existingScriptIds = expected.map((script) => script.id).sort(); + const expectedScriptIds = actual.map((script) => script.id).sort(); + expect(existingScriptIds).to.deep.equal(expectedScriptIds, formatAssertionMessage([ + 'Unexpected script IDs.', + `Expected: ${expectedScriptIds.join(', ')}`, + `Actual: ${existingScriptIds.join(', ')}`, + ])); +} + +function expectSameRevertStates( + actual: readonly SelectedScript[], + expected: readonly SelectedScript[], +) { + const scriptsWithDifferentRevertStates = actual + .filter((script) => { + const other = expected.find((existing) => existing.id === script.id); + if (!other) { + throw new Error(`Script "${script.id}" does not exist in expected scripts: ${JSON.stringify(expected, null, '\t')}`); + } + return script.revert !== other.revert; + }); + expect(scriptsWithDifferentRevertStates).to.have.lengthOf(0, formatAssertionMessage([ + 'Scripts with different revert states:', + scriptsWithDifferentRevertStates + .map((s) => indentText([ + `Script ID: "${s.id}"`, + `Actual revert state: "${s.revert}"`, + `Expected revert state: "${expected.find((existing) => existing.id === s.id)?.revert ?? 'unknown'}"`, + ].join('\n'))) + .join('\n---\n'), + ])); +} diff --git a/tests/unit/application/Context/State/Selection/Script/UserSelectedScript.spec.ts b/tests/unit/application/Context/State/Selection/Script/UserSelectedScript.spec.ts new file mode 100644 index 00000000..edc03223 --- /dev/null +++ b/tests/unit/application/Context/State/Selection/Script/UserSelectedScript.spec.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { UserSelectedScript } from '@/application/Context/State/Selection/Script/UserSelectedScript'; + +describe('UserSelectedScript', () => { + it('id is same as script id', () => { + // arrange + const expectedId = 'scriptId'; + const script = new ScriptStub(expectedId); + const sut = new UserSelectedScript(script, false); + // act + const actualId = sut.id; + // assert + expect(actualId).to.equal(expectedId); + }); + it('throws when revert is true for irreversible script', () => { + // arrange + const scriptId = 'irreversibleScriptId'; + const expectedError = `The script with ID '${scriptId}' is not reversible and cannot be reverted.`; + const script = new ScriptStub(scriptId) + .withRevertCode(undefined); + // act + const act = () => new UserSelectedScript(script, true); + // assert + expect(act).to.throw(expectedError); + }); +}); diff --git a/tests/unit/application/Context/State/Selection/UserSelectionFacade.spec.ts b/tests/unit/application/Context/State/Selection/UserSelectionFacade.spec.ts new file mode 100644 index 00000000..4bf6e05b --- /dev/null +++ b/tests/unit/application/Context/State/Selection/UserSelectionFacade.spec.ts @@ -0,0 +1,133 @@ +import { describe, it } from 'vitest'; +import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; +import { UserSelectionFacade } from '@/application/Context/State/Selection/UserSelectionFacade'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; +import type { ScriptsFactory, CategoriesFactory } from '@/application/Context/State/Selection/UserSelectionFacade'; +import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub'; +import { CategorySelectionStub } from '@tests/unit/shared/Stubs/CategorySelectionStub'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import type { ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; + +describe('UserSelectionFacade', () => { + describe('ctor', () => { + describe('scripts', () => { + it('constructs with expected collection', () => { + // arrange + const expectedCollection = new CategoryCollectionStub(); + let actualCollection: ICategoryCollection | undefined; + const factoryMock: ScriptsFactory = (collection) => { + actualCollection = collection; + return new ScriptSelectionStub(); + }; + const builder = new UserSelectionFacadeBuilder() + .withCollection(expectedCollection) + .withScriptsFactory(factoryMock); + // act + builder.construct(); + // assert + expectExists(actualCollection); + expect(actualCollection).to.equal(expectedCollection); + }); + it('constructs with expected selected scripts', () => { + // arrange + const expectedScripts: readonly SelectedScript[] = [ + new SelectedScriptStub(new ScriptStub('1')), + ]; + let actualScripts: readonly SelectedScript[] | undefined; + const factoryMock: ScriptsFactory = (_, scripts) => { + actualScripts = scripts; + return new ScriptSelectionStub(); + }; + const builder = new UserSelectionFacadeBuilder() + .withSelectedScripts(expectedScripts) + .withScriptsFactory(factoryMock); + // act + builder.construct(); + // assert + expectExists(actualScripts); + expect(actualScripts).to.equal(expectedScripts); + }); + }); + describe('categories', () => { + it('constructs with expected collection', () => { + // arrange + const expectedCollection = new CategoryCollectionStub(); + let actualCollection: ICategoryCollection | undefined; + const factoryMock: CategoriesFactory = (_, collection) => { + actualCollection = collection; + return new CategorySelectionStub(); + }; + const builder = new UserSelectionFacadeBuilder() + .withCollection(expectedCollection) + .withCategoriesFactory(factoryMock); + // act + builder.construct(); + // assert + expectExists(actualCollection); + expect(actualCollection).to.equal(expectedCollection); + }); + it('constructs with expected scripts', () => { + // arrange + const expectedScriptSelection = new ScriptSelectionStub(); + let actualScriptsSelection: ScriptSelection | undefined; + const categoriesFactoryMock: CategoriesFactory = (selection) => { + actualScriptsSelection = selection; + return new CategorySelectionStub(); + }; + const scriptsFactoryMock: ScriptsFactory = () => { + return expectedScriptSelection; + }; + const builder = new UserSelectionFacadeBuilder() + .withCategoriesFactory(categoriesFactoryMock) + .withScriptsFactory(scriptsFactoryMock); + // act + builder.construct(); + // assert + expectExists(actualScriptsSelection); + expect(actualScriptsSelection).to.equal(expectedScriptSelection); + }); + }); + }); +}); + +class UserSelectionFacadeBuilder { + private collection: ICategoryCollection = new CategoryCollectionStub(); + + private selectedScripts: readonly SelectedScript[] = []; + + private scriptsFactory: ScriptsFactory = () => new ScriptSelectionStub(); + + private categoriesFactory: CategoriesFactory = () => new CategorySelectionStub(); + + public withCollection(collection: ICategoryCollection): this { + this.collection = collection; + return this; + } + + public withSelectedScripts(selectedScripts: readonly SelectedScript[]): this { + this.selectedScripts = selectedScripts; + return this; + } + + public withScriptsFactory(scriptsFactory: ScriptsFactory): this { + this.scriptsFactory = scriptsFactory; + return this; + } + + public withCategoriesFactory(categoriesFactory: CategoriesFactory): this { + this.categoriesFactory = categoriesFactory; + return this; + } + + public construct(): UserSelectionFacade { + return new UserSelectionFacade( + this.collection, + this.selectedScripts, + this.scriptsFactory, + this.categoriesFactory, + ); + } +} diff --git a/tests/unit/application/Parser/ApplicationParser.spec.ts b/tests/unit/application/Parser/ApplicationParser.spec.ts new file mode 100644 index 00000000..58bdbd07 --- /dev/null +++ b/tests/unit/application/Parser/ApplicationParser.spec.ts @@ -0,0 +1,197 @@ +import { describe, it, expect } from 'vitest'; +import type { CollectionData } from '@/application/collections/'; +import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser'; +import { parseApplication } from '@/application/Parser/ApplicationParser'; +import WindowsData from '@/application/collections/windows.yaml'; +import MacOsData from '@/application/collections/macos.yaml'; +import LinuxData from '@/application/collections/linux.yaml'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; +import { CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub'; +import { CategoryCollectionParserStub } from '@tests/unit/shared/Stubs/CategoryCollectionParserStub'; +import { ProjectDetailsParserStub } from '@tests/unit/shared/Stubs/ProjectDetailsParserStub'; +import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'; +import type { CategoryCollectionParser } from '@/application/Parser/CategoryCollectionParser'; +import type { NonEmptyCollectionAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; + +describe('ApplicationParser', () => { + describe('parseApplication', () => { + describe('categoryParser', () => { + it('returns result from the parser', () => { + // arrange + const os = OperatingSystem.macOS; + const data = new CollectionDataStub(); + const expected = new CategoryCollectionStub() + .withOs(os); + const parser = new CategoryCollectionParserStub() + .withReturnValue(data, expected) + .getStub(); + const sut = new ApplicationParserBuilder() + .withCategoryCollectionParser(parser) + .withCollectionsData([data]); + // act + const app = sut.parseApplication(); + // assert + const actual = app.getCollection(os); + expect(expected).to.equal(actual); + }); + }); + describe('projectDetails', () => { + it('projectDetailsParser is used to create the instance', () => { + // arrange + const expectedProjectDetails = new ProjectDetailsStub(); + const projectDetailsParserStub = new ProjectDetailsParserStub() + .withReturnValue(expectedProjectDetails); + const sut = new ApplicationParserBuilder() + .withProjectDetailsParser(projectDetailsParserStub.getStub()); + // act + const app = sut.parseApplication(); + // assert + const actualProjectDetails = app.projectDetails; + expect(expectedProjectDetails).to.deep.equal(actualProjectDetails); + }); + it('projectDetailsParser is used to parse collection', () => { + // arrange + const expectedProjectDetails = new ProjectDetailsStub(); + const projectDetailsParserStub = new ProjectDetailsParserStub() + .withReturnValue(expectedProjectDetails); + const collectionParserStub = new CategoryCollectionParserStub(); + const sut = new ApplicationParserBuilder() + .withProjectDetailsParser(projectDetailsParserStub.getStub()) + .withCategoryCollectionParser(collectionParserStub.getStub()); + // act + sut.parseApplication(); + // assert + expect(collectionParserStub.arguments).to.have.length.above(0); + const actuallyUsedInfos = collectionParserStub.arguments.map((arg) => arg.projectDetails); + expect(actuallyUsedInfos.every( + (actualProjectDetails) => actualProjectDetails === expectedProjectDetails, + )).to.equal(true); + }); + }); + describe('collectionsData', () => { + describe('set as expected', () => { + // arrange + const testScenarios: { + readonly description: string; + readonly input: readonly CollectionData[]; + readonly output: readonly ICategoryCollection[]; + }[] = [ + { + description: 'single collection', + input: [new CollectionDataStub()], + output: [new CategoryCollectionStub().withOs(OperatingSystem.macOS)], + }, + { + description: 'multiple collections', + input: [ + new CollectionDataStub().withOs('windows'), + new CollectionDataStub().withOs('macos'), + ], + output: [ + new CategoryCollectionStub().withOs(OperatingSystem.macOS), + new CategoryCollectionStub().withOs(OperatingSystem.Windows), + ], + }, + ]; + // act + testScenarios.forEach(({ + description, input, output, + }) => { + it(description, () => { + let categoryParserStub = new CategoryCollectionParserStub(); + for (let i = 0; i < input.length; i++) { + categoryParserStub = categoryParserStub + .withReturnValue(input[i], output[i]); + } + const sut = new ApplicationParserBuilder() + .withCategoryCollectionParser(categoryParserStub.getStub()) + .withCollectionsData(input); + // act + const app = sut.parseApplication(); + // assert + expect(app.collections).to.deep.equal(output); + }); + }); + }); + it('defaults to expected data', () => { + // arrange + const expected = [WindowsData, MacOsData, LinuxData]; + const categoryParserStub = new CategoryCollectionParserStub(); + const sut = new ApplicationParserBuilder() + .withCollectionsData(undefined) + .withCategoryCollectionParser(categoryParserStub.getStub()); + // act + sut.parseApplication(); + // assert + const actual = categoryParserStub.arguments.map((args) => args.data); + expect(actual).to.deep.equal(expected); + }); + it('validates non empty array', () => { + // arrange + const data = [new CollectionDataStub()]; + const expectedAssertion: NonEmptyCollectionAssertion = { + value: data, + valueName: 'Collections', + }; + const validator = new TypeValidatorStub(); + const sut = new ApplicationParserBuilder() + .withCollectionsData(data) + .withTypeValidator(validator); + // act + sut.parseApplication(); + // assert + validator.expectNonEmptyCollectionAssertion(expectedAssertion); + }); + }); + }); +}); + +class ApplicationParserBuilder { + private categoryCollectionParser + : CategoryCollectionParser = new CategoryCollectionParserStub().getStub(); + + private projectDetailsParser + : typeof parseProjectDetails = new ProjectDetailsParserStub().getStub(); + + private validator: TypeValidator = new TypeValidatorStub(); + + private collectionsData: readonly CollectionData[] | undefined = [new CollectionDataStub()]; + + public withCategoryCollectionParser( + categoryCollectionParser: CategoryCollectionParser, + ): this { + this.categoryCollectionParser = categoryCollectionParser; + return this; + } + + public withProjectDetailsParser( + projectDetailsParser: typeof parseProjectDetails, + ): this { + this.projectDetailsParser = projectDetailsParser; + return this; + } + + public withCollectionsData(collectionsData: readonly CollectionData[] | undefined): this { + this.collectionsData = collectionsData; + return this; + } + + public withTypeValidator(validator: TypeValidator): this { + this.validator = validator; + return this; + } + + public parseApplication(): ReturnType { + return parseApplication( + this.collectionsData, + { + parseCategoryCollection: this.categoryCollectionParser, + parseProjectDetails: this.projectDetailsParser, + validator: this.validator, + }, + ); + } +} diff --git a/tests/unit/application/Parser/CategoryCollectionParser.spec.ts b/tests/unit/application/Parser/CategoryCollectionParser.spec.ts new file mode 100644 index 00000000..c27f7cd6 --- /dev/null +++ b/tests/unit/application/Parser/CategoryCollectionParser.spec.ts @@ -0,0 +1,316 @@ +import { describe, it, expect } from 'vitest'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import { parseCategoryCollection, type CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser'; +import { type CategoryParser } from '@/application/Parser/Executable/CategoryParser'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub'; +import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'; +import { getCategoryStub, CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub'; +import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub'; +import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub'; +import type { CollectionData, ScriptingDefinitionData, FunctionData } from '@/application/collections/'; +import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; +import type { NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import type { EnumParser } from '@/application/Common/Enum'; +import type { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser'; +import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub'; +import type { CategoryCollectionSpecificUtilitiesFactory } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities'; +import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub'; +import { CategoryParserStub } from '@tests/unit/shared/Stubs/CategoryParserStub'; +import { createCategoryCollectionFactorySpy } from '@tests/unit/shared/Stubs/CategoryCollectionFactoryStub'; +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; +import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; + +describe('CategoryCollectionParser', () => { + describe('parseCategoryCollection', () => { + it('validates object', () => { + // arrange + const data = new CollectionDataStub(); + const expectedAssertion: ObjectAssertion = { + value: data, + valueName: 'Collection', + allowedProperties: [ + 'os', 'scripting', 'actions', 'functions', + ], + }; + const validator = new TypeValidatorStub(); + const context = new TestContext() + .withData(data) + .withTypeValidator(validator); + // act + context.parseCategoryCollection(); + // assert + validator.expectObjectAssertion(expectedAssertion); + }); + describe('actions', () => { + it('validates non-empty collection', () => { + // arrange + const actions = [getCategoryStub('test1'), getCategoryStub('test2')]; + const expectedAssertion: NonEmptyCollectionAssertion = { + value: actions, + valueName: '\'actions\' in collection', + }; + const validator = new TypeValidatorStub(); + const context = new TestContext() + .withData(new CollectionDataStub().withActions(actions)) + .withTypeValidator(validator); + // act + context.parseCategoryCollection(); + // assert + validator.expectNonEmptyCollectionAssertion(expectedAssertion); + }); + it('parses actions correctly', () => { + // arrange + const { + categoryCollectionFactorySpy, + getInitParameters, + } = createCategoryCollectionFactorySpy(); + const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')]; + const expectedActions = [ + new CategoryStub('expected-action-1'), + new CategoryStub('expected-action-2'), + ]; + const categoryParserStub = new CategoryParserStub() + .withConfiguredParseResult(actionsData[0], expectedActions[0]) + .withConfiguredParseResult(actionsData[1], expectedActions[1]); + const collectionData = new CollectionDataStub() + .withActions(actionsData); + const context = new TestContext() + .withData(collectionData) + .withCategoryParser(categoryParserStub.get()) + .withCategoryCollectionFactory(categoryCollectionFactorySpy); + // act + const actualCollection = context.parseCategoryCollection(); + // assert + const actualActions = getInitParameters(actualCollection)?.actions; + expect(actualActions).to.have.lengthOf(expectedActions.length); + expect(actualActions).to.have.members(expectedActions); + }); + describe('utilities', () => { + it('parses actions with correct utilities', () => { + // arrange + const expectedUtilities = new CategoryCollectionSpecificUtilitiesStub(); + const utilitiesFactory: CategoryCollectionSpecificUtilitiesFactory = () => { + return expectedUtilities; + }; + const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')]; + const collectionData = new CollectionDataStub() + .withActions(actionsData); + const categoryParserStub = new CategoryParserStub(); + const context = new TestContext() + .withData(collectionData) + .withCollectionUtilitiesFactory(utilitiesFactory) + .withCategoryParser(categoryParserStub.get()); + // act + context.parseCategoryCollection(); + // assert + const usedUtilities = categoryParserStub.getUsedUtilities(); + expect(usedUtilities).to.have.lengthOf(2); + expect(usedUtilities[0]).to.equal(expectedUtilities); + expect(usedUtilities[1]).to.equal(expectedUtilities); + }); + describe('construction', () => { + it('creates utilities with correct functions data', () => { + // arrange + const expectedFunctionsData = [createFunctionDataWithCode()]; + const collectionData = new CollectionDataStub() + .withFunctions(expectedFunctionsData); + let actualFunctionsData: ReadonlyArray | undefined; + const utilitiesFactory: CategoryCollectionSpecificUtilitiesFactory = (data) => { + actualFunctionsData = data; + return new CategoryCollectionSpecificUtilitiesStub(); + }; + const context = new TestContext() + .withData(collectionData) + .withCollectionUtilitiesFactory(utilitiesFactory); + // act + context.parseCategoryCollection(); + // assert + expect(actualFunctionsData).to.equal(expectedFunctionsData); + }); + it('creates utilities with correct scripting definition', () => { + // arrange + const expectedScripting = new ScriptingDefinitionStub(); + const scriptingDefinitionParser: ScriptingDefinitionParser = () => { + return expectedScripting; + }; + let actualScripting: IScriptingDefinition | undefined; + const utilitiesFactory: CategoryCollectionSpecificUtilitiesFactory = (_, scripting) => { + actualScripting = scripting; + return new CategoryCollectionSpecificUtilitiesStub(); + }; + const context = new TestContext() + .withCollectionUtilitiesFactory(utilitiesFactory) + .withScriptDefinitionParser(scriptingDefinitionParser); + // act + context.parseCategoryCollection(); + // assert + expect(actualScripting).to.equal(expectedScripting); + }); + }); + }); + }); + describe('scripting definition', () => { + it('parses correctly', () => { + // arrange + const { + categoryCollectionFactorySpy, + getInitParameters, + } = createCategoryCollectionFactorySpy(); + const expected = new ScriptingDefinitionStub(); + const scriptingDefinitionParser: ScriptingDefinitionParser = () => { + return expected; + }; + const context = new TestContext() + .withCategoryCollectionFactory(categoryCollectionFactorySpy) + .withScriptDefinitionParser(scriptingDefinitionParser); + // act + const actualCategoryCollection = context.parseCategoryCollection(); + // assert + const actualScripting = getInitParameters(actualCategoryCollection)?.scripting; + expect(expected).to.equal(actualScripting); + }); + it('parses expected data', () => { + // arrange + const expectedData = new ScriptingDefinitionDataStub(); + const collection = new CollectionDataStub() + .withScripting(expectedData); + let actualData: ScriptingDefinitionData | undefined; + const scriptingDefinitionParser + : ScriptingDefinitionParser = (data: ScriptingDefinitionData) => { + actualData = data; + return new ScriptingDefinitionStub(); + }; + const context = new TestContext() + .withScriptDefinitionParser(scriptingDefinitionParser) + .withData(collection); + // act + context.parseCategoryCollection(); + // assert + expect(actualData).to.equal(expectedData); + }); + it('parses with correct project details', () => { + // arrange + const expectedProjectDetails = new ProjectDetailsStub(); + let actualDetails: ProjectDetails | undefined; + const scriptingDefinitionParser + : ScriptingDefinitionParser = (_, details: ProjectDetails) => { + actualDetails = details; + return new ScriptingDefinitionStub(); + }; + const context = new TestContext() + .withProjectDetails(expectedProjectDetails) + .withScriptDefinitionParser(scriptingDefinitionParser); + // act + context.parseCategoryCollection(); + // assert + expect(actualDetails).to.equal(expectedProjectDetails); + }); + }); + describe('os', () => { + it('parses correctly', () => { + // arrange + const { + categoryCollectionFactorySpy, + getInitParameters, + } = createCategoryCollectionFactorySpy(); + const expectedOs = OperatingSystem.macOS; + const osText = 'macos'; + const expectedName = 'os'; + const collectionData = new CollectionDataStub() + .withOs(osText); + const parserMock = new EnumParserStub() + .setup(expectedName, osText, expectedOs); + const context = new TestContext() + .withOsParser(parserMock) + .withCategoryCollectionFactory(categoryCollectionFactorySpy) + .withData(collectionData); + // act + const actualCollection = context.parseCategoryCollection(); + // assert + const actualOs = getInitParameters(actualCollection)?.os; + expect(actualOs).to.equal(expectedOs); + }); + }); + }); +}); + +class TestContext { + private data: CollectionData = new CollectionDataStub(); + + private projectDetails: ProjectDetails = new ProjectDetailsStub(); + + private validator: TypeValidator = new TypeValidatorStub(); + + private osParser: EnumParser = new EnumParserStub() + .setupDefaultValue(OperatingSystem.Android); + + private collectionUtilitiesFactory + : CategoryCollectionSpecificUtilitiesFactory = () => { + return new CategoryCollectionSpecificUtilitiesStub(); + }; + + private scriptDefinitionParser: ScriptingDefinitionParser = () => new ScriptingDefinitionStub(); + + private categoryParser: CategoryParser = new CategoryParserStub().get(); + + private categoryCollectionFactory + : CategoryCollectionFactory = createCategoryCollectionFactorySpy().categoryCollectionFactorySpy; + + public withData(data: CollectionData): this { + this.data = data; + return this; + } + + public withCategoryParser(categoryParser: CategoryParser): this { + this.categoryParser = categoryParser; + return this; + } + + public withCategoryCollectionFactory(categoryCollectionFactory: CategoryCollectionFactory): this { + this.categoryCollectionFactory = categoryCollectionFactory; + return this; + } + + public withProjectDetails(projectDetails: ProjectDetails): this { + this.projectDetails = projectDetails; + return this; + } + + public withOsParser(osParser: EnumParser): this { + this.osParser = osParser; + return this; + } + + public withScriptDefinitionParser(scriptDefinitionParser: ScriptingDefinitionParser): this { + this.scriptDefinitionParser = scriptDefinitionParser; + return this; + } + + public withTypeValidator(typeValidator: TypeValidator): this { + this.validator = typeValidator; + return this; + } + + public withCollectionUtilitiesFactory( + collectionUtilitiesFactory: CategoryCollectionSpecificUtilitiesFactory, + ): this { + this.collectionUtilitiesFactory = collectionUtilitiesFactory; + return this; + } + + public parseCategoryCollection(): ReturnType { + return parseCategoryCollection( + this.data, + this.projectDetails, + { + osParser: this.osParser, + validator: this.validator, + parseScriptingDefinition: this.scriptDefinitionParser, + createUtilities: this.collectionUtilitiesFactory, + parseCategory: this.categoryParser, + createCategoryCollection: this.categoryCollectionFactory, + }, + ); + } +} diff --git a/tests/unit/application/Parser/Common/ContextualError.spec.ts b/tests/unit/application/Parser/Common/ContextualError.spec.ts new file mode 100644 index 00000000..a4124f2a --- /dev/null +++ b/tests/unit/application/Parser/Common/ContextualError.spec.ts @@ -0,0 +1,191 @@ +import { describe, it, expect } from 'vitest'; +import { CustomError } from '@/application/Common/CustomError'; +import { wrapErrorWithAdditionalContext } from '@/application/Parser/Common/ContextualError'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { indentText } from '@/application/Common/Text/IndentText'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('wrapErrorWithAdditionalContext', () => { + it(`extend ${CustomError.name}`, () => { + // arrange + const expected = CustomError; + // act + const error = new TestContext() + .build(); + // assert + expect(error).to.be.an.instanceof(expected); + }); + describe('inner error preservation', () => { + it('preserves the original error', () => { + // arrange + const expectedError = new Error(); + const context = new TestContext() + .withInnerError(expectedError); + // act + const error = context.build(); + // assert + const actualError = getInnerErrorFromContextualError(error); + expect(actualError).to.equal(expectedError); + }); + it('sets the original error as the cause', () => { + // arrange + const expectedError = new Error('error causing the issue'); + const context = new TestContext() + .withInnerError(expectedError); + // act + const error = context.build(); + // assert + const actualError = error.cause; + expect(actualError).to.equal(expectedError); + }); + }); + describe('error message construction', () => { + it('includes the original error message', () => { + // arrange + const expectedOriginalErrorMessage = 'Message from the inner error'; + + // act + const error = new TestContext() + .withInnerError(new Error(expectedOriginalErrorMessage)) + .build(); + + // assert + expect(error.message).contains(expectedOriginalErrorMessage); + }); + it('includes original error toString() if message is absent', () => { + // arrange + const originalError = new Error(); + const expectedPartInMessage = originalError.toString(); + + // act + const error = new TestContext() + .withInnerError(originalError) + .build(); + + // assert + expect(error.message).contains(expectedPartInMessage); + }); + it('appends additional context to the error message', () => { + // arrange + const expectedAdditionalContext = 'Expected additional context message'; + + // act + const error = new TestContext() + .withAdditionalContext(expectedAdditionalContext) + .build(); + + // assert + expect(error.message).contains(expectedAdditionalContext); + }); + describe('message order', () => { + it('displays the latest context before the original error message', () => { + // arrange + const originalErrorMessage = 'Original message from the inner error to be shown first'; + const additionalContext = 'Context to be displayed after'; + + // act + const error = new TestContext() + .withInnerError(new Error(originalErrorMessage)) + .withAdditionalContext(additionalContext) + .build(); + + // assert + expectMessageDisplayOrder(error.message, { + firstMessage: additionalContext, + secondMessage: originalErrorMessage, + }); + }); + it('appends multiple contexts from most specific to most general', () => { + // arrange + const deepErrorContext = 'first-context'; + const parentErrorContext = 'second-context'; + + // act + const deepError = new TestContext() + .withAdditionalContext(deepErrorContext) + .build(); + const parentError = new TestContext() + .withInnerError(deepError) + .withAdditionalContext(parentErrorContext) + .build(); + const grandParentError = new TestContext() + .withInnerError(parentError) + .withAdditionalContext('latest-error') + .build(); + + // assert + expectMessageDisplayOrder(grandParentError.message, { + firstMessage: deepErrorContext, + secondMessage: parentErrorContext, + }); + }); + }); + }); + describe('throws error when context is missing', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'Missing additional context'; + const context = new TestContext() + .withAdditionalContext(absentValue); + // act + const act = () => context.build(); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); +}); + +function expectMessageDisplayOrder( + actualMessage: string, + expectation: { + readonly firstMessage: string; + readonly secondMessage: string; + }, +): void { + const firstMessageIndex = actualMessage.indexOf(expectation.firstMessage); + const secondMessageIndex = actualMessage.indexOf(expectation.secondMessage); + expect(firstMessageIndex).to.be.lessThan(secondMessageIndex, formatAssertionMessage([ + 'Error output order does not match the expected order.', + 'Expected the first message to be displayed before the second message.', + 'Expected first message:', + indentText(expectation.firstMessage), + 'Expected second message:', + indentText(expectation.secondMessage), + 'Received message:', + indentText(actualMessage), + ])); +} + +class TestContext { + private innerError: Error = new Error(`[${TestContext.name}] original error`); + + private additionalContext = `[${TestContext.name}] additional context`; + + public withInnerError(innerError: Error) { + this.innerError = innerError; + return this; + } + + public withAdditionalContext(additionalContext: string) { + this.additionalContext = additionalContext; + return this; + } + + public build(): ReturnType { + return wrapErrorWithAdditionalContext( + this.innerError, + this.additionalContext, + ); + } +} + +function getInnerErrorFromContextualError(error: Error & { + readonly context?: { + readonly innerError?: Error; + }, +}): Error { + if (error?.context?.innerError instanceof Error) { + return error.context.innerError; + } + throw new Error('Error must have a context with a valid innerError property.'); +} diff --git a/tests/unit/application/Parser/Common/ContextualErrorTester.ts b/tests/unit/application/Parser/Common/ContextualErrorTester.ts new file mode 100644 index 00000000..0bbf7112 --- /dev/null +++ b/tests/unit/application/Parser/Common/ContextualErrorTester.ts @@ -0,0 +1,53 @@ +import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { indentText } from '@/application/Common/Text/IndentText'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub'; + +interface ContextualErrorTestScenario { + readonly throwingAction: (wrapError: ErrorWithContextWrapper) => void; + readonly expectedWrappedError: Error; + readonly expectedContextMessage: string; +} + +export function itThrowsContextualError( + testScenario: ContextualErrorTestScenario, +) { + it('throws wrapped error', () => { + // arrange + const expectedError = new Error(); + const wrapperStub = new ErrorWrapperStub() + .withError(expectedError); + // act + const act = () => testScenario.throwingAction(wrapperStub.get()); + // assert + expect(act).to.throw(expectedError); + }); + it('wraps internal error', () => { + // arrange + const expectedInternalError = testScenario.expectedWrappedError; + const wrapperStub = new ErrorWrapperStub(); + // act + try { + testScenario.throwingAction(wrapperStub.get()); + } catch { /* Swallow */ } + // assert + expect(wrapperStub.lastError).to.deep.equal(expectedInternalError); + }); + it('includes expected context', () => { + // arrange + const { expectedContextMessage: expectedContext } = testScenario; + const wrapperStub = new ErrorWrapperStub(); + // act + try { + testScenario.throwingAction(wrapperStub.get()); + } catch { /* Swallow */ } + // assert + expectExists(wrapperStub.lastContext); + expect(wrapperStub.lastContext).to.equal(expectedContext, formatAssertionMessage([ + 'Unexpected additional context (additional message added to the wrapped error).', + `Actual additional context:\n${indentText(wrapperStub.lastContext)}`, + `Expected additional context:\n${indentText(expectedContext)}`, + ])); + }); +} diff --git a/tests/unit/application/Parser/Common/TypeValidator.spec.ts b/tests/unit/application/Parser/Common/TypeValidator.spec.ts new file mode 100644 index 00000000..410d55c9 --- /dev/null +++ b/tests/unit/application/Parser/Common/TypeValidator.spec.ts @@ -0,0 +1,259 @@ +import { describe, it, expect } from 'vitest'; +import { createTypeValidator, type NonEmptyStringAssertion, type RegexValidationRule } from '@/application/Parser/Common/TypeValidator'; +import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('createTypeValidator', () => { + describe('assertObject', () => { + describe('with valid object', () => { + it('accepts object with allowed properties', () => { + // arrange + const expectedProperties = ['expected1', 'expected2']; + const validValue = createObjectWithProperties(expectedProperties); + const { assertObject } = createTypeValidator(); + // act + const act = () => assertObject({ + value: validValue, + valueName: 'unimportant name', + allowedProperties: expectedProperties, + }); + // assert + expect(act).to.not.throw(); + }); + it('accepts object with extra unspecified properties', () => { + // arrange + const validValue = createObjectWithProperties(['unevaluated property']); + const { assertObject } = createTypeValidator(); + // act + const act = () => assertObject({ + value: validValue, + valueName: 'unimportant name', + }); + // assert + expect(act).to.not.throw(); + }); + }); + describe('with invalid object', () => { + describe('throws error for missing object', () => { + itEachAbsentObjectValue((absentValue) => { + // arrange + const valueName = 'absent object value'; + const expectedMessage = `'${valueName}' is missing.`; + const { assertObject } = createTypeValidator(); + // act + const act = () => assertObject({ value: absentValue, valueName }); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + it('throws error for object without properties', () => { + // arrange + const emptyObjectValue: object = {}; + const valueName = 'empty object without properties.'; + const expectedMessage = `'${valueName}' is an empty object without properties.`; + const { assertObject } = createTypeValidator(); + // act + const act = () => assertObject({ value: emptyObjectValue, valueName }); + // assert + expect(act).to.throw(expectedMessage); + }); + describe('incorrect data type', () => { + // arrange + const testScenarios: readonly { + readonly value: unknown; + readonly valueName: string; + }[] = [ + { + value: ['1', '2'], + valueName: 'array of strings', + }, + { + value: true, + valueName: 'true boolean', + }, + { + value: 35, + valueName: 'number', + }, + ]; + testScenarios.forEach(({ value, valueName }) => { + it(`throws error for ${valueName} passed as object`, () => { + // arrange + const expectedMessage = `'${valueName}' is not an object.`; + const { assertObject } = createTypeValidator(); + // act + const act = () => assertObject({ value, valueName }); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + }); + it('throws error for object with disallowed properties', () => { + // arrange + const valueName = 'value with unexpected properties'; + const unexpectedProperties = ['unexpected-property-1', 'unexpected-property-2']; + const expectedError = `'${valueName}' has disallowed properties: ${unexpectedProperties.join(', ')}.`; + const expectedProperties = ['expected1', 'expected2']; + const value = createObjectWithProperties( + [...expectedProperties, ...unexpectedProperties], + ); + const { assertObject } = createTypeValidator(); + // act + const act = () => assertObject({ + value, + valueName, + allowedProperties: expectedProperties, + }); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); + describe('assertNonEmptyCollection', () => { + describe('with valid collection', () => { + it('accepts non-empty collection', () => { + // arrange + const validValue = ['array', 'of', 'strings']; + const { assertNonEmptyCollection } = createTypeValidator(); + // act + const act = () => assertNonEmptyCollection({ value: validValue, valueName: 'unimportant name' }); + // assert + expect(act).to.not.throw(); + }); + }); + describe('with invalid collection', () => { + describe('throws error for missing collection', () => { + itEachAbsentObjectValue((absentValue) => { + // arrange + const valueName = 'absent collection value'; + const expectedMessage = `'${valueName}' is missing.`; + const { assertNonEmptyCollection } = createTypeValidator(); + // act + const act = () => assertNonEmptyCollection({ value: absentValue, valueName }); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + it('throws error for empty collection', () => { + // arrange + const emptyArrayValue = []; + const valueName = 'empty collection value'; + const expectedMessage = `'${valueName}' cannot be an empty array.`; + const { assertNonEmptyCollection } = createTypeValidator(); + // act + const act = () => assertNonEmptyCollection({ value: emptyArrayValue, valueName }); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + }); + describe('assertNonEmptyString', () => { + describe('with valid string', () => { + it('accepts non-empty string without regex rule', () => { + // arrange + const nonEmptyString = 'hello'; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString({ value: nonEmptyString, valueName: 'unimportant name' }); + // assert + expect(act).to.not.throw(); + }); + it('accepts if the string matches the regex', () => { + // arrange + const regex: RegExp = /goodbye/; + const stringMatchingRegex = 'Valid string containing "goodbye"'; + const rule: RegexValidationRule = { + expectedMatch: regex, + errorMessage: 'String contain "goodbye"', + }; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString({ + value: stringMatchingRegex, + valueName: 'unimportant name', + rule, + }); + // assert + expect(act).to.not.throw(); + }); + }); + describe('with invalid string', () => { + describe('throws error for missing string', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const valueName = 'absent string value'; + const expectedMessage = `'${valueName}' is missing.`; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString({ value: absentValue, valueName }); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + describe('throws error for non string values', () => { + // arrange + const testScenarios: readonly { + readonly description: string; + readonly invalidValue: unknown; + }[] = [ + { + description: 'number', + invalidValue: 42, + }, + { + description: 'boolean', + invalidValue: true, + }, + { + description: 'object', + invalidValue: { property: 'value' }, + }, + { + description: 'array', + invalidValue: ['a', 'r', 'r', 'a', 'y'], + }, + ]; + testScenarios.forEach(({ + description, invalidValue, + }) => { + it(description, () => { + const valueName = 'invalidValue'; + const expectedMessage = `${valueName} should be of type 'string', but is of type '${typeof invalidValue}'.`; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString({ value: invalidValue, valueName }); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + }); + it('throws an error if the string does not match the regex', () => { + // arrange + const regex: RegExp = /goodbye/; + const stringNotMatchingRegex = 'Hello'; + const expectedMessage = 'String should contain "goodbye"'; + const rule: RegexValidationRule = { + expectedMatch: regex, + errorMessage: expectedMessage, + }; + const assertion: NonEmptyStringAssertion = { + value: stringNotMatchingRegex, + valueName: 'non-important-value-name', + rule, + }; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString(assertion); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + }); +}); + +function createObjectWithProperties(properties: readonly string[]): object { + const object = {}; + properties.forEach((propertyName) => { + object[propertyName] = 'arbitrary value'; + }); + return object; +} diff --git a/tests/unit/application/Parser/Executable/CategoryCollectionSpecificUtilities.spec.ts b/tests/unit/application/Parser/Executable/CategoryCollectionSpecificUtilities.spec.ts new file mode 100644 index 00000000..f09372f6 --- /dev/null +++ b/tests/unit/application/Parser/Executable/CategoryCollectionSpecificUtilities.spec.ts @@ -0,0 +1,103 @@ +import { describe, it, expect } from 'vitest'; +import type { ISyntaxFactory } from '@/application/Parser/Executable/Script/Validation/Syntax/ISyntaxFactory'; +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; +import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub'; +import type { FunctionData } from '@/application/collections/'; +import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub'; +import { ScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompiler'; +import { createCollectionUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities'; +import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import { createSyntaxFactoryStub } from '@tests/unit/shared/Stubs/SyntaxFactoryStub'; + +describe('CategoryCollectionSpecificUtilities', () => { + describe('createCollectionUtilities', () => { + describe('functionsData', () => { + describe('can create with absent data', () => { + itEachAbsentCollectionValue((absentValue) => { + // arrange + const context = new TextContext() + .withData(absentValue); + // act + const act = () => context.createCollectionUtilities(); + // assert + expect(act).to.not.throw(); + }, { excludeNull: true }); + }); + }); + }); + describe('compiler', () => { + it('constructed as expected', () => { + // arrange + const functionsData = [createFunctionDataWithCode()]; + const syntax = new LanguageSyntaxStub(); + const expected = new ScriptCompiler({ + functions: functionsData, + syntax, + }); + const language = ScriptingLanguage.shellscript; + const factoryMock = createSyntaxFactoryStub(language, syntax); + const definition = new ScriptingDefinitionStub() + .withLanguage(language); + const context = new TextContext() + .withData(functionsData) + .withScripting(definition) + .withSyntaxFactory(factoryMock); + // act + const utilities = context.createCollectionUtilities(); + // assert + const actual = utilities.compiler; + expect(actual).to.deep.equal(expected); + }); + }); + describe('syntax', () => { + it('set from syntax factory', () => { + // arrange + const language = ScriptingLanguage.shellscript; + const expected = new LanguageSyntaxStub(); + const factoryMock = createSyntaxFactoryStub(language, expected); + const definition = new ScriptingDefinitionStub() + .withLanguage(language); + const context = new TextContext() + .withScripting(definition) + .withSyntaxFactory(factoryMock); + // act + const utilities = context.createCollectionUtilities(); + // assert + const actual = utilities.syntax; + expect(actual).to.equal(expected); + }); + }); +}); + +class TextContext { + private functionsData: readonly FunctionData[] | undefined = [createFunctionDataWithCode()]; + + private scripting: IScriptingDefinition = new ScriptingDefinitionStub(); + + private syntaxFactory: ISyntaxFactory = createSyntaxFactoryStub(); + + public withScripting(scripting: IScriptingDefinition): this { + this.scripting = scripting; + return this; + } + + public withData(data: readonly FunctionData[] | undefined): this { + this.functionsData = data; + return this; + } + + public withSyntaxFactory(syntaxFactory: ISyntaxFactory): this { + this.syntaxFactory = syntaxFactory; + return this; + } + + public createCollectionUtilities(): ReturnType { + return createCollectionUtilities( + this.functionsData, + this.scripting, + this.syntaxFactory, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/CategoryParser.spec.ts b/tests/unit/application/Parser/Executable/CategoryParser.spec.ts new file mode 100644 index 00000000..40a1642c --- /dev/null +++ b/tests/unit/application/Parser/Executable/CategoryParser.spec.ts @@ -0,0 +1,530 @@ +import { describe, it, expect } from 'vitest'; +import type { CategoryData, ExecutableData } from '@/application/collections/'; +import { parseCategory } from '@/application/Parser/Executable/CategoryParser'; +import { type ScriptParser } from '@/application/Parser/Executable/Script/ScriptParser'; +import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser'; +import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub'; +import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub'; +import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType'; +import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub'; +import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub'; +import type { ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator'; +import { ExecutableValidatorStub, createExecutableValidatorFactoryStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub'; +import type { CategoryErrorContext, UnknownExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext'; +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; +import { createCategoryFactorySpy } from '@tests/unit/shared/Stubs/CategoryFactoryStub'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator'; +import { indentText } from '@/application/Common/Text/IndentText'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; +import type { CategoryFactory } from '@/domain/Executables/Category/CategoryFactory'; +import { itThrowsContextualError } from '../Common/ContextualErrorTester'; +import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester'; +import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator'; + +describe('CategoryParser', () => { + describe('parseCategory', () => { + describe('id', () => { + it('creates ID correctly', () => { + // arrange + const expectedId: ExecutableId = 'expected-id'; + const categoryData = new CategoryDataStub() + .withName(expectedId); + const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); + // act + const actualScript = new TestContext() + .withData(categoryData) + .withCategoryFactory(categoryFactorySpy) + .parseCategory(); + // assert + const actualId = getInitParameters(actualScript)?.executableId; + expect(actualId).to.equal(expectedId); + }); + }); + describe('name', () => { + it('parses name correctly', () => { + // arrange + const expectedName = 'test-expected-name'; + const categoryData = new CategoryDataStub() + .withName(expectedName); + const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); + // act + const actualCategory = new TestContext() + .withData(categoryData) + .withCategoryFactory(categoryFactorySpy) + .parseCategory(); + // assert + const actualName = getInitParameters(actualCategory)?.name; + expect(actualName).to.equal(expectedName); + }); + describe('validates name', () => { + // arrange + const expectedName = 'expected category name to be validated'; + const category = new CategoryDataStub() + .withName(expectedName); + const expectedContext: CategoryErrorContext = { + type: ExecutableType.Category, + self: category, + }; + itValidatesName((validatorFactory) => { + // act + new TestContext() + .withData(category) + .withValidatorFactory(validatorFactory) + .parseCategory(); + // assert + return { + expectedNameToValidate: expectedName, + expectedErrorContext: expectedContext, + }; + }); + }); + }); + describe('docs', () => { + it('parses docs correctly', () => { + // arrange + const url = 'https://privacy.sexy'; + const categoryData = new CategoryDataStub() + .withDocs(url); + const parseDocs: DocsParser = (data) => { + return [ + `parsed docs: ${JSON.stringify(data)}`, + ]; + }; + const expectedDocs = parseDocs(categoryData); + const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); + // act + const actualCategory = new TestContext() + .withData(categoryData) + .withCategoryFactory(categoryFactorySpy) + .withDocsParser(parseDocs) + .parseCategory(); + // assert + const actualDocs = getInitParameters(actualCategory)?.docs; + expect(actualDocs).to.deep.equal(expectedDocs); + }); + }); + describe('property validation', () => { + describe('validates for unknown executable', () => { + // arrange + const category = new CategoryDataStub(); + const expectedContext: CategoryErrorContext = { + type: ExecutableType.Category, + self: category, + }; + const expectedAssertion: ObjectAssertion = { + value: category, + valueName: 'Executable', + }; + itValidatesType( + (validatorFactory) => { + // act + new TestContext() + .withData(category) + .withValidatorFactory(validatorFactory) + .parseCategory(); + // assert + return { + assertValidation: (validator) => validator.assertObject(expectedAssertion), + expectedErrorContext: expectedContext, + }; + }, + ); + }); + describe('validates for category', () => { + // arrange + const category = new CategoryDataStub(); + const expectedContext: CategoryErrorContext = { + type: ExecutableType.Category, + self: category, + }; + const expectedAssertion: ObjectAssertion = { + value: category, + valueName: category.category, + allowedProperties: ['docs', 'children', 'category'], + }; + itValidatesType( + (validatorFactory) => { + // act + new TestContext() + .withData(category) + .withValidatorFactory(validatorFactory) + .parseCategory(); + // assert + return { + assertValidation: (validator) => validator.assertObject(expectedAssertion), + expectedErrorContext: expectedContext, + }; + }, + ); + }); + }); + describe('children', () => { + describe('validates children for non-empty collection', () => { + // arrange + const category = new CategoryDataStub() + .withChildren([createScriptDataWithCode()]); + const expectedContext: CategoryErrorContext = { + type: ExecutableType.Category, + self: category, + }; + const expectedAssertion: NonEmptyCollectionAssertion = { + value: category.children, + valueName: category.category, + }; + itValidatesType( + (validatorFactory) => { + // act + new TestContext() + .withData(category) + .withValidatorFactory(validatorFactory) + .parseCategory(); + // assert + return { + assertValidation: (validator) => validator.assertObject(expectedAssertion), + expectedErrorContext: expectedContext, + }; + }, + ); + }); + describe('validates that a child is a category or a script', () => { + // arrange + const testScenarios = generateDataValidationTestScenarios({ + expectFail: [{ + description: 'child has incorrect properties', + data: { property: 'non-empty-value' } as unknown as ExecutableData, + }], + expectPass: [ + { + description: 'child is a category', + data: new CategoryDataStub(), + }, + { + description: 'child is a script with call', + data: createScriptDataWithCall(), + }, + { + description: 'child is a script with code', + data: createScriptDataWithCode(), + }, + ], + }); + testScenarios.forEach(({ + description, expectedPass, data: childData, + }) => { + describe(description, () => { + itAsserts({ + expectedConditionResult: expectedPass, + test: (validatorFactory) => { + const expectedError = 'Executable is neither a category or a script.'; + const parent = new CategoryDataStub() + .withName('parent') + .withChildren([new CategoryDataStub().withName('valid child'), childData]); + const expectedContext: UnknownExecutableErrorContext = { + self: childData, + parentCategory: parent, + }; + // act + new TestContext() + .withData(parent) + .withValidatorFactory(validatorFactory) + .parseCategory(); + // assert + return { + expectedErrorMessage: expectedError, + expectedErrorContext: expectedContext, + }; + }, + }); + }); + }); + }); + describe('validates children recursively', () => { + describe('validates (1th-level) child type', () => { + // arrange + const expectedName = 'child category'; + const child = new CategoryDataStub() + .withName(expectedName); + const parent = new CategoryDataStub() + .withName('parent') + .withChildren([child]); + const expectedContext: UnknownExecutableErrorContext = { + self: child, + parentCategory: parent, + }; + const expectedAssertion: ObjectAssertion = { + value: child, + valueName: 'Executable', + }; + itValidatesType( + (validatorFactory) => { + // act + new TestContext() + .withData(parent) + .withValidatorFactory(validatorFactory) + .parseCategory(); + // assert + return { + assertValidation: (validator) => validator.assertObject(expectedAssertion), + expectedErrorContext: expectedContext, + }; + }, + ); + }); + describe('validates that (2nd-level) child name', () => { + // arrange + const expectedName = 'grandchild category'; + const grandChild = new CategoryDataStub() + .withName(expectedName); + const child = new CategoryDataStub() + .withChildren([grandChild]) + .withName('child category'); + const parent = new CategoryDataStub() + .withName('parent') + .withChildren([child]); + const expectedContext: CategoryErrorContext = { + type: ExecutableType.Category, + self: grandChild, + parentCategory: child, + }; + itValidatesName((validatorFactory) => { + // act + new TestContext() + .withData(parent) + .withValidatorFactory(validatorFactory) + .parseCategory(); + // assert + return { + expectedNameToValidate: expectedName, + expectedErrorContext: expectedContext, + }; + }); + }); + }); + describe('parses correct subscript', () => { + it('parses single script correctly', () => { + // arrange + const expectedScript = new ScriptStub('expected script'); + const scriptParser = new ScriptParserStub(); + const childScriptData = createScriptDataWithCode(); + const categoryData = new CategoryDataStub() + .withChildren([childScriptData]); + scriptParser.setupParsedResultForData(childScriptData, expectedScript); + const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); + // act + const actualCategory = new TestContext() + .withData(categoryData) + .withScriptParser(scriptParser.get()) + .withCategoryFactory(categoryFactorySpy) + .parseCategory(); + // assert + const actualScripts = getInitParameters(actualCategory)?.scripts; + expectExists(actualScripts); + expect(actualScripts).to.have.lengthOf(1); + const actualScript = actualScripts[0]; + expect(actualScript).to.equal(expectedScript); + }); + it('parses multiple scripts correctly', () => { + // arrange + const expectedScripts = [ + new ScriptStub('expected-first-script'), + new ScriptStub('expected-second-script'), + ]; + const childrenData = [ + createScriptDataWithCall(), + createScriptDataWithCode(), + ]; + const scriptParser = new ScriptParserStub(); + childrenData.forEach((_, index) => { + scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]); + }); + const categoryData = new CategoryDataStub() + .withChildren(childrenData); + const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); + // act + const actualCategory = new TestContext() + .withScriptParser(scriptParser.get()) + .withData(categoryData) + .withCategoryFactory(categoryFactorySpy) + .parseCategory(); + // assert + const actualParsedScripts = getInitParameters(actualCategory)?.scripts; + expectExists(actualParsedScripts); + expect(actualParsedScripts.length).to.equal(expectedScripts.length); + expect(actualParsedScripts).to.have.members(expectedScripts); + }); + it('parses all scripts with correct utilities', () => { + // arrange + const expected = new CategoryCollectionSpecificUtilitiesStub(); + const scriptParser = new ScriptParserStub(); + const childrenData = [ + createScriptDataWithCode(), + createScriptDataWithCode(), + createScriptDataWithCode(), + ]; + const categoryData = new CategoryDataStub() + .withChildren(childrenData); + const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); + // act + const actualCategory = new TestContext() + .withData(categoryData) + .withCollectionUtilities(expected) + .withScriptParser(scriptParser.get()) + .withCategoryFactory(categoryFactorySpy) + .parseCategory(); + // assert + const actualParsedScripts = getInitParameters(actualCategory)?.scripts; + expectExists(actualParsedScripts); + const actualUtilities = actualParsedScripts.map( + (s) => scriptParser.getParseParameters(s)[1], + ); + expect( + actualUtilities.every( + (actual) => actual === expected, + ), + formatAssertionMessage([ + `Expected all elements to be ${JSON.stringify(expected)}`, + 'All elements:', + indentText(JSON.stringify(actualUtilities)), + ]), + ).to.equal(true); + }); + }); + it('parses correct subcategories', () => { + // arrange + const expectedChildCategory = new CategoryStub('expected-child-category'); + const childCategoryData = new CategoryDataStub() + .withName('expected child category') + .withChildren([createScriptDataWithCode()]); + const categoryData = new CategoryDataStub() + .withName('category name') + .withChildren([childCategoryData]); + const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); + // act + const actualCategory = new TestContext() + .withData(categoryData) + .withCategoryFactory((parameters) => { + if (parameters.name === childCategoryData.category) { + return expectedChildCategory; + } + return categoryFactorySpy(parameters); + }) + .parseCategory(); + // assert + const actualSubcategories = getInitParameters(actualCategory)?.subcategories; + expectExists(actualSubcategories); + expect(actualSubcategories).to.have.lengthOf(1); + expect(actualSubcategories[0]).to.equal(expectedChildCategory); + }); + }); + describe('category creation', () => { + it('creates category from the factory', () => { + // arrange + const expectedCategory = new CategoryStub('expected-category'); + const categoryFactory: CategoryFactory = () => expectedCategory; + // act + const actualCategory = new TestContext() + .withCategoryFactory(categoryFactory) + .parseCategory(); + // assert + expect(actualCategory).to.equal(expectedCategory); + }); + describe('rethrows exception if category factory fails', () => { + // arrange + const givenData = new CategoryDataStub(); + const expectedContextMessage = 'Failed to parse category.'; + const expectedError = new Error(); + // act & assert + itThrowsContextualError({ + throwingAction: (wrapError) => { + const validatorStub = new ExecutableValidatorStub(); + validatorStub.createContextualErrorMessage = (message) => message; + const factoryMock: CategoryFactory = () => { + throw expectedError; + }; + new TestContext() + .withCategoryFactory(factoryMock) + .withValidatorFactory(() => validatorStub) + .withErrorWrapper(wrapError) + .withData(givenData) + .parseCategory(); + }, + expectedWrappedError: expectedError, + expectedContextMessage, + }); + }); + }); + }); +}); + +class TestContext { + private data: CategoryData = new CategoryDataStub(); + + private collectionUtilities: + CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub(); + + private categoryFactory: CategoryFactory = createCategoryFactorySpy().categoryFactorySpy; + + private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get(); + + private validatorFactory: ExecutableValidatorFactory = createExecutableValidatorFactoryStub; + + private docsParser: DocsParser = () => ['docs']; + + private scriptParser: ScriptParser = new ScriptParserStub().get(); + + public withData(data: CategoryData) { + this.data = data; + return this; + } + + public withCollectionUtilities( + collectionUtilities: CategoryCollectionSpecificUtilitiesStub, + ): this { + this.collectionUtilities = collectionUtilities; + return this; + } + + public withCategoryFactory(categoryFactory: CategoryFactory): this { + this.categoryFactory = categoryFactory; + return this; + } + + public withValidatorFactory(validatorFactory: ExecutableValidatorFactory): this { + this.validatorFactory = validatorFactory; + return this; + } + + public withErrorWrapper(errorWrapper: ErrorWithContextWrapper): this { + this.errorWrapper = errorWrapper; + return this; + } + + public withScriptParser(scriptParser: ScriptParser): this { + this.scriptParser = scriptParser; + return this; + } + + public withDocsParser(docsParser: DocsParser): this { + this.docsParser = docsParser; + return this; + } + + public parseCategory() { + return parseCategory( + this.data, + this.collectionUtilities, + { + createCategory: this.categoryFactory, + wrapError: this.errorWrapper, + createValidator: this.validatorFactory, + parseScript: this.scriptParser, + parseDocs: this.docsParser, + }, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/DocumentationParser.spec.ts b/tests/unit/application/Parser/Executable/DocumentationParser.spec.ts new file mode 100644 index 00000000..678d74b6 --- /dev/null +++ b/tests/unit/application/Parser/Executable/DocumentationParser.spec.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from 'vitest'; +import type { DocumentableData } from '@/application/collections/'; +import { parseDocs } from '@/application/Parser/Executable/DocumentationParser'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('DocumentationParser', () => { + describe('parseDocs', () => { + describe('throws when single documentation is missing', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing documentation'; + const data: DocumentableData = { docs: ['non empty doc 1', absentValue] }; + // act + const act = () => parseDocs(data); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + describe('throws when type is unexpected', () => { + // arrange + const expectedTypeError = 'docs field (documentation) must be a single string or an array of strings.'; + const wrongTypedValue = 22 as never; + const testCases: ReadonlyArray<{ + readonly name: string; + readonly data: DocumentableData; + }> = [ + { + name: 'given docs', + data: { docs: wrongTypedValue }, + }, + { + name: 'single doc', + data: { docs: ['non empty doc 1', wrongTypedValue] }, + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + // act + const act = () => parseDocs(testCase.data); + // assert + expect(act).to.throw(expectedTypeError); + }); + } + }); + it('returns empty when empty', () => { + // arrange + const empty: DocumentableData = { }; + // act + const actual = parseDocs(empty); + // assert + expect(actual).to.have.lengthOf(0); + }); + it('returns single item when string', () => { + // arrange + const url = 'https://privacy.sexy'; + const expected = [url]; + const sut: DocumentableData = { docs: url }; + // act + const actual = parseDocs(sut); + // assert + expect(actual).to.deep.equal(expected); + }); + it('returns all when array', () => { + // arrange + const expected = ['https://privacy.sexy', 'https://github.com/undergroundwires/privacy.sexy']; + const sut: DocumentableData = { docs: expected }; + // act + const actual = parseDocs(sut); + // assert + expect(actual).to.deep.equal(expected); + }); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/Expression.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/Expression.spec.ts new file mode 100644 index 00000000..36fff888 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/Expression.spec.ts @@ -0,0 +1,240 @@ +import { describe, it, expect } from 'vitest'; +import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition'; +import { type ExpressionEvaluator, Expression } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/Expression'; +import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; +import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub'; +import { FunctionCallArgumentStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentStub'; +import { ExpressionEvaluationContextStub } from '@tests/unit/shared/Stubs/ExpressionEvaluationContextStub'; +import type { IPipelineCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/IPipelineCompiler'; +import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub'; +import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection'; +import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import type { IExpressionEvaluationContext } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; + +describe('Expression', () => { + describe('ctor', () => { + describe('position', () => { + it('sets as expected', () => { + // arrange + const expected = new ExpressionPosition(0, 5); + // act + const actual = new ExpressionBuilder() + .withPosition(expected) + .build(); + // assert + expect(actual.position).to.equal(expected); + }); + }); + describe('parameters', () => { + describe('defaults to empty array if absent', () => { + itEachAbsentObjectValue((absentValue) => { + // arrange + const parameters = absentValue; + // act + const actual = new ExpressionBuilder() + .withParameters(parameters) + .build(); + // assert + expect(actual.parameters); + expect(actual.parameters.all); + expect(actual.parameters.all.length).to.equal(0); + }, { excludeNull: true }); + }); + it('sets as expected', () => { + // arrange + const expected = new FunctionParameterCollectionStub() + .withParameterName('firstParameterName') + .withParameterName('secondParameterName'); + // act + const actual = new ExpressionBuilder() + .withParameters(expected) + .build(); + // assert + expect(actual.parameters).to.deep.equal(expected); + }); + }); + }); + describe('evaluate', () => { + describe('throws with invalid arguments', () => { + const testCases: readonly { + name: string, + context: IExpressionEvaluationContext, + expectedError: string, + sutBuilder?: (builder: ExpressionBuilder) => ExpressionBuilder, + }[] = [ + { + name: 'throws when some of the required args are not provided', + sutBuilder: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b', 'c'], false), + context: new ExpressionEvaluationContextStub() + .withArgs(new FunctionCallArgumentCollectionStub().withArgument('b', 'provided')), + expectedError: 'argument values are provided for required parameters: "a", "c"', + }, + { + name: 'throws when none of the required args are not provided', + sutBuilder: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b'], false), + context: new ExpressionEvaluationContextStub() + .withArgs(new FunctionCallArgumentCollectionStub().withArgument('c', 'unrelated')), + expectedError: 'argument values are provided for required parameters: "a", "b"', + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + // arrange + const sutBuilder = new ExpressionBuilder(); + if (testCase.sutBuilder) { + testCase.sutBuilder(sutBuilder); + } + const sut = sutBuilder.build(); + // act + const act = () => sut.evaluate(testCase.context); + // assert + expect(act).to.throw(testCase.expectedError); + }); + } + }); + it('returns result from evaluator', () => { + // arrange + const evaluatorMock: ExpressionEvaluator = (c) => `"${c + .args + .getAllParameterNames() + .map((name) => context.args.getArgument(name)) + .map((arg) => `${arg.parameterName}': '${arg.argumentValue}'`) + .join('", "')}"`; + const givenArguments = new FunctionCallArgumentCollectionStub() + .withArgument('parameter1', 'value1') + .withArgument('parameter2', 'value2'); + const expectedParameterNames = givenArguments.getAllParameterNames(); + const context = new ExpressionEvaluationContextStub() + .withArgs(givenArguments); + const expected = evaluatorMock(context); + const sut = new ExpressionBuilder() + .withEvaluator(evaluatorMock) + .withParameterNames(expectedParameterNames) + .build(); + // arrange + const actual = sut.evaluate(context); + // assert + expect(expected).to.equal(actual, formatAssertionMessage([ + `Given arguments: ${JSON.stringify(givenArguments)}`, + `Expected parameter names: ${JSON.stringify(expectedParameterNames)}`, + ])); + }); + it('sends pipeline compiler as it is', () => { + // arrange + const expected = new PipelineCompilerStub(); + const context = new ExpressionEvaluationContextStub() + .withPipelineCompiler(expected); + let actual: IPipelineCompiler | undefined; + const evaluatorMock: ExpressionEvaluator = (c) => { + actual = c.pipelineCompiler; + return ''; + }; + const sut = new ExpressionBuilder() + .withEvaluator(evaluatorMock) + .build(); + // arrange + sut.evaluate(context); + // assert + expectExists(actual); + expect(expected).to.equal(actual); + }); + describe('filters unused parameters', () => { + // arrange + const testCases = [ + { + name: 'with a provided argument', + expressionParameters: new FunctionParameterCollectionStub() + .withParameterName('parameterToHave', false), + arguments: new FunctionCallArgumentCollectionStub() + .withArgument('parameterToHave', 'value-to-have') + .withArgument('parameterToIgnore', 'value-to-ignore'), + expectedArguments: [ + new FunctionCallArgumentStub() + .withParameterName('parameterToHave').withArgumentValue('value-to-have'), + ], + }, + { + name: 'without a provided argument', + expressionParameters: new FunctionParameterCollectionStub() + .withParameterName('parameterToHave', false) + .withParameterName('parameterToIgnore', true), + arguments: new FunctionCallArgumentCollectionStub() + .withArgument('parameterToHave', 'value-to-have'), + expectedArguments: [ + new FunctionCallArgumentStub() + .withParameterName('parameterToHave').withArgumentValue('value-to-have'), + ], + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + let actual: IReadOnlyFunctionCallArgumentCollection | undefined; + const evaluatorMock: ExpressionEvaluator = (c) => { + actual = c.args; + return ''; + }; + const context = new ExpressionEvaluationContextStub() + .withArgs(testCase.arguments); + const sut = new ExpressionBuilder() + .withEvaluator(evaluatorMock) + .withParameters(testCase.expressionParameters) + .build(); + // act + sut.evaluate(context); + // assert + expectExists(actual); + const collection = actual; + const actualArguments = collection.getAllParameterNames() + .map((name) => collection.getArgument(name)); + expect(actualArguments).to.deep.equal(testCase.expectedArguments); + }); + } + }); + }); +}); + +class ExpressionBuilder { + private position: ExpressionPosition = new ExpressionPosition(0, 5); + + private parameters?: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub(); + + public withPosition(position: ExpressionPosition) { + this.position = position; + return this; + } + + public withEvaluator(evaluator: ExpressionEvaluator) { + this.evaluator = evaluator; + return this; + } + + public withParameters(parameters: IReadOnlyFunctionParameterCollection | undefined) { + this.parameters = parameters; + return this; + } + + public withParameterName(parameterName: string, isOptional = true) { + const collection = new FunctionParameterCollectionStub() + .withParameterName(parameterName, isOptional); + return this.withParameters(collection); + } + + public withParameterNames(parameterNames: string[], isOptional = true) { + const collection = new FunctionParameterCollectionStub() + .withParameterNames(parameterNames, isOptional); + return this.withParameters(collection); + } + + public build() { + return new Expression({ + position: this.position, + evaluator: this.evaluator, + parameters: this.parameters, + }); + } + + private evaluator: ExpressionEvaluator = () => `[${ExpressionBuilder.name}] evaluated-expression`; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.spec.ts new file mode 100644 index 00000000..9bb2b100 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.spec.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; +import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; +import type { IPipelineCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/IPipelineCompiler'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; +import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub'; + +describe('ExpressionEvaluationContext', () => { + describe('ctor', () => { + describe('args', () => { + it('sets as expected', () => { + // arrange + const expected = new FunctionCallArgumentCollectionStub() + .withArgument('expectedParameter', 'expectedValue'); + const builder = new ExpressionEvaluationContextBuilder() + .withArgs(expected); + // act + const sut = builder.build(); + // assert + const actual = sut.args; + expect(actual).to.equal(expected); + }); + }); + describe('pipelineCompiler', () => { + it('sets as expected', () => { + // arrange + const expected = new PipelineCompilerStub(); + const builder = new ExpressionEvaluationContextBuilder() + .withPipelineCompiler(expected); + // act + const sut = builder.build(); + // assert + expect(sut.pipelineCompiler).to.equal(expected); + }); + }); + }); +}); + +class ExpressionEvaluationContextBuilder { + private args: IReadOnlyFunctionCallArgumentCollection = new FunctionCallArgumentCollectionStub(); + + private pipelineCompiler: IPipelineCompiler = new PipelineCompilerStub(); + + public withArgs(args: IReadOnlyFunctionCallArgumentCollection) { + this.args = args; + return this; + } + + public withPipelineCompiler(pipelineCompiler: IPipelineCompiler) { + this.pipelineCompiler = pipelineCompiler; + return this; + } + + public build(): IExpressionEvaluationContext { + return new ExpressionEvaluationContext(this.args, this.pipelineCompiler); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition.spec.ts new file mode 100644 index 00000000..eb707dd6 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition.spec.ts @@ -0,0 +1,213 @@ +import { describe, it, expect } from 'vitest'; +import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition'; + +describe('ExpressionPosition', () => { + describe('ctor', () => { + it('sets as expected', () => { + // arrange + const expectedStart = 0; + const expectedEnd = 5; + // act + const sut = new ExpressionPosition(expectedStart, expectedEnd); + // assert + expect(sut.start).to.equal(expectedStart); + expect(sut.end).to.equal(expectedEnd); + }); + describe('throws when invalid', () => { + // arrange + const testCases = [ + { start: 5, end: 5, error: 'no length (start = end = 5)' }, + { start: 5, end: 3, error: 'start (5) after end (3)' }, + { start: -1, end: 3, error: 'negative start position: -1' }, + ]; + for (const testCase of testCases) { + it(testCase.error, () => { + // act + const act = () => new ExpressionPosition(testCase.start, testCase.end); + // assert + expect(act).to.throw(testCase.error); + }); + } + }); + }); + describe('isInInsideOf', () => { + // arrange + const testCases: readonly { + name: string, + sut: ExpressionPosition, + potentialParent: ExpressionPosition, + expectedResult: boolean, + }[] = [ + { + name: 'true; when other contains sut inside boundaries', + sut: new ExpressionPosition(4, 8), + potentialParent: new ExpressionPosition(0, 10), + expectedResult: true, + }, + { + name: 'true; when other contains sut with same upper boundary', + sut: new ExpressionPosition(4, 10), + potentialParent: new ExpressionPosition(0, 10), + expectedResult: true, + }, + { + name: 'true; when other contains sut with same lower boundary', + sut: new ExpressionPosition(0, 8), + potentialParent: new ExpressionPosition(0, 10), + expectedResult: true, + }, + { + name: 'false; when other is same as sut', + sut: new ExpressionPosition(0, 10), + potentialParent: new ExpressionPosition(0, 10), + expectedResult: false, + }, + { + name: 'false; when sut contains other', + sut: new ExpressionPosition(0, 10), + potentialParent: new ExpressionPosition(4, 8), + expectedResult: false, + }, + { + name: 'false; when sut starts and ends before other', + sut: new ExpressionPosition(0, 10), + potentialParent: new ExpressionPosition(15, 25), + expectedResult: false, + }, + { + name: 'false; when sut starts before other but ends inside other', + sut: new ExpressionPosition(0, 10), + potentialParent: new ExpressionPosition(5, 10), + expectedResult: false, + }, + { + name: 'false; when sut starts inside other but ends after other', + sut: new ExpressionPosition(5, 11), + potentialParent: new ExpressionPosition(0, 10), + expectedResult: false, + }, + { + name: 'false; when sut starts at same position but end after other', + sut: new ExpressionPosition(0, 11), + potentialParent: new ExpressionPosition(0, 10), + expectedResult: false, + }, + { + name: 'false; when sut ends at same positions but start before other', + sut: new ExpressionPosition(0, 10), + potentialParent: new ExpressionPosition(1, 10), + expectedResult: false, + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + // act + const actual = testCase.sut.isInInsideOf(testCase.potentialParent); + // assert + expect(actual).to.equal(testCase.expectedResult); + }); + } + }); + describe('isSame', () => { + // arrange + const testCases: readonly { + name: string, + sut: ExpressionPosition, + other: ExpressionPosition, + expectedResult: boolean, + }[] = [ + { + name: 'true; when positions are same', + sut: new ExpressionPosition(0, 10), + other: new ExpressionPosition(0, 10), + expectedResult: true, + }, + { + name: 'false; when start position is different', + sut: new ExpressionPosition(0, 10), + other: new ExpressionPosition(1, 10), + expectedResult: false, + }, + { + name: 'false; when end position is different', + sut: new ExpressionPosition(0, 10), + other: new ExpressionPosition(0, 11), + expectedResult: false, + }, + { + name: 'false; when both start and end positions are different', + sut: new ExpressionPosition(0, 10), + other: new ExpressionPosition(20, 30), + expectedResult: false, + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + // act + const actual = testCase.sut.isSame(testCase.other); + // assert + expect(actual).to.equal(testCase.expectedResult); + }); + } + }); + describe('isIntersecting', () => { + // arrange + const testCases: readonly { + name: string, + first: ExpressionPosition, + second: ExpressionPosition, + expectedResult: boolean, + }[] = [ + { + name: 'true; when one contains other inside boundaries', + first: new ExpressionPosition(4, 8), + second: new ExpressionPosition(0, 10), + expectedResult: true, + }, + { + name: 'true; when one starts inside other\'s ending boundary without being contained', + first: new ExpressionPosition(0, 10), + second: new ExpressionPosition(9, 15), + expectedResult: true, + }, + { + name: 'true; when positions are the same', + first: new ExpressionPosition(0, 5), + second: new ExpressionPosition(0, 5), + expectedResult: true, + }, + { + name: 'true; when one starts inside other\'s starting boundary without being contained', + first: new ExpressionPosition(5, 10), + second: new ExpressionPosition(5, 11), + expectedResult: true, + }, + { + name: 'false; when one starts directly after other', + first: new ExpressionPosition(0, 10), + second: new ExpressionPosition(10, 20), + expectedResult: false, + }, + { + name: 'false; when one starts after other with margin', + first: new ExpressionPosition(0, 10), + second: new ExpressionPosition(100, 200), + expectedResult: false, + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + // act + const actual = testCase.first.isIntersecting(testCase.second); + // assert + expect(actual).to.equal(testCase.expectedResult); + }); + it(`reversed: ${testCase.name}`, () => { + // act + const actual = testCase.second.isIntersecting(testCase.first); + // assert + expect(actual).to.equal(testCase.expectedResult); + }); + } + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.spec.ts new file mode 100644 index 00000000..165a6beb --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.spec.ts @@ -0,0 +1,99 @@ +import { describe, it, expect } from 'vitest'; +import { createPositionFromRegexFullMatch } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPositionFactory'; +import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition'; +import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests'; + +describe('ExpressionPositionFactory', () => { + describe('createPositionFromRegexFullMatch', () => { + describe('it is a transient factory', () => { + // arrange + const fakeMatch = createRegexMatch(); + // act + const create = () => createPositionFromRegexFullMatch(fakeMatch); + // assert + itIsTransientFactory({ + getter: create, + expectedType: ExpressionPosition, + }); + }); + it('creates a position with the correct start position', () => { + // arrange + const expectedStartPosition = 5; + const fakeMatch = createRegexMatch({ + fullMatch: 'matched string', + matchIndex: expectedStartPosition, + }); + // act + const position = createPositionFromRegexFullMatch(fakeMatch); + // assert + expect(position.start).toBe(expectedStartPosition); + }); + + it('creates a position with the correct end position', () => { + // arrange + const startPosition = 3; + const matchedString = 'matched string'; + const expectedEndPosition = startPosition + matchedString.length; + const fakeMatch = createRegexMatch({ + fullMatch: matchedString, + matchIndex: startPosition, + }); + // act + const position = createPositionFromRegexFullMatch(fakeMatch); + // assert + expect(position.end).to.equal(expectedEndPosition); + }); + + it('creates correct position with capturing groups', () => { + // arrange + const startPosition = 20; + const fakeMatch = createRegexMatch({ + fullMatch: 'matched string', + capturingGroups: ['group1', 'group2'], + matchIndex: startPosition, + }); + // act + const position = createPositionFromRegexFullMatch(fakeMatch); + // assert + expect(position.start).toBe(startPosition); + expect(position.end).toBe(startPosition + fakeMatch[0].length); + }); + + describe('invalid values', () => { + it('throws an error if match.index is undefined', () => { + // arrange + const fakeMatch = createRegexMatch(); + fakeMatch.index = undefined; + const expectedError = `Regex match did not yield any results: ${JSON.stringify(fakeMatch)}`; + // act + const act = () => createPositionFromRegexFullMatch(fakeMatch); + // assert + expect(act).to.throw(expectedError); + }); + it('throws an error for empty match', () => { + // arrange + const fakeMatch = createRegexMatch({ + fullMatch: '', + matchIndex: 0, + }); + const expectedError = `Regex match is empty: ${JSON.stringify(fakeMatch)}`; + // act + const act = () => createPositionFromRegexFullMatch(fakeMatch); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); +}); + +function createRegexMatch(options?: { + readonly fullMatch?: string, + readonly capturingGroups?: readonly string[], + readonly matchIndex?: number, +}): RegExpMatchArray { + const fullMatch = options?.fullMatch ?? 'default fake match'; + const capturingGroups = options?.capturingGroups ?? []; + const fakeMatch: RegExpMatchArray = [fullMatch, ...capturingGroups]; + fakeMatch.index = options?.matchIndex ?? 0; + return fakeMatch; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler.spec.ts new file mode 100644 index 00000000..7bc6e105 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler.spec.ts @@ -0,0 +1,330 @@ +import { describe, it, expect } from 'vitest'; +import { ExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler'; +import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser'; +import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub'; +import { ExpressionParserStub } from '@tests/unit/shared/Stubs/ExpressionParserStub'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import type { IExpression } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/IExpression'; + +describe('ExpressionsCompiler', () => { + describe('compileExpressions', () => { + describe('returns empty string when code is absent', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expected = ''; + const code = absentValue; + const sut = new SystemUnderTest(); + const args = new FunctionCallArgumentCollectionStub(); + // act + const value = sut.compileExpressions(code, args); + // assert + expect(value).to.equal(expected); + }, { excludeNull: true, excludeUndefined: true }); + }); + describe('can compile nested expressions', () => { + it('when one expression is evaluated to a text that contains another expression', () => { + // arrange + const expectedResult = 'hello world!'; + const rawCode = 'hello {{ firstExpression }}!'; + const outerExpressionResult = '{{ secondExpression }}'; + const expectedCodeAfterFirstCompilationRound = 'hello {{ secondExpression }}!'; + const innerExpressionResult = 'world'; + const expressionParserMock = new ExpressionParserStub() + .withResult(rawCode, [ + new ExpressionStub() + // {{ firstExpression } + .withPosition(6, 27) + // Parser would hit the outer expression + .withEvaluatedResult(outerExpressionResult), + ]) + .withResult(expectedCodeAfterFirstCompilationRound, [ + new ExpressionStub() + // {{ secondExpression }} + .withPosition(6, 28) + // once the outer expression parser, compiler now parses its evaluated result + .withEvaluatedResult(innerExpressionResult), + ]); + const sut = new SystemUnderTest(expressionParserMock); + const args = new FunctionCallArgumentCollectionStub(); + // act + const actual = sut.compileExpressions(rawCode, args); + // assert + expect(actual).to.equal(expectedResult); + }); + describe('when one expression contains another hardcoded expression', () => { + it('when hardcoded expression is does not contain the hardcoded expression', () => { + // arrange + const expectedResult = 'hi !'; + const rawCode = 'hi {{ outerExpressionStart }}delete {{ innerExpression }} me{{ outerExpressionEnd }}!'; + const outerExpressionResult = ''; + const innerExpressionResult = 'should not be there'; + const expressionParserMock = new ExpressionParserStub() + .withResult(rawCode, [ + new ExpressionStub() + // {{ outerExpressionStart }}delete {{ innerExpression }} me{{ outerExpressionEnd }} + .withPosition(3, 84) + .withEvaluatedResult(outerExpressionResult), + new ExpressionStub() + // {{ innerExpression }} + .withPosition(36, 57) + // Parser would hit both expressions as one is hardcoded in other + .withEvaluatedResult(innerExpressionResult), + ]) + .withResult(expectedResult, []); + const sut = new SystemUnderTest(expressionParserMock); + const args = new FunctionCallArgumentCollectionStub(); + // act + const actual = sut.compileExpressions(rawCode, args); + // assert + expect(actual).to.equal(expectedResult); + }); + it('when hardcoded expression contains the hardcoded expression', () => { + // arrange + const expectedResult = 'hi game of thrones!'; + const rawCode = 'hi {{ outerExpressionStart }} game {{ innerExpression }} {{ outerExpressionEnd }}!'; + const expectedCodeAfterFirstCompilationRound = 'hi game {{ innerExpression }}!'; // outer is compiled first + const outerExpressionResult = 'game {{ innerExpression }}'; + const innerExpressionResult = 'of thrones'; + const expressionParserMock = new ExpressionParserStub() + .withResult(rawCode, [ + new ExpressionStub() + // {{ outerExpressionStart }} game {{ innerExpression }} {{ outerExpressionEnd }} + .withPosition(3, 81) + // Parser would hit the outer expression + .withEvaluatedResult(outerExpressionResult), + new ExpressionStub() + // {{ innerExpression }} + .withPosition(35, 57) + // Parser would hit both expressions as one is hardcoded in other + .withEvaluatedResult(innerExpressionResult), + ]) + .withResult(expectedCodeAfterFirstCompilationRound, [ + new ExpressionStub() + // {{ innerExpression }} + .withPosition(8, 29) + // once the outer expression parser, compiler now parses its evaluated result + .withEvaluatedResult(innerExpressionResult), + ]); + const sut = new SystemUnderTest(expressionParserMock); + const args = new FunctionCallArgumentCollectionStub(); + // act + const actual = sut.compileExpressions(rawCode, args); + // assert + expect(actual).to.equal(expectedResult); + }); + }); + }); + describe('combines expressions as expected', () => { + // arrange + const code = 'part1 {{ a }} part2 {{ b }} part3'; + const testCases = [ + { + name: 'with ordered expressions', + expressions: [ + new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'), + new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'), + ], + expected: 'part1 a part2 b part3', + }, + { + name: 'unordered expressions', + expressions: [ + new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'), + new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'), + ], + expected: 'part1 a part2 b part3', + }, + { + name: 'with an optional expected argument that is not provided', + expressions: [ + new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a') + .withParameterNames(['optionalParameter'], true), + new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b') + .withParameterNames(['optionalParameterTwo'], true), + ], + expected: 'part1 a part2 b part3', + }, + { + name: 'with no expressions', + expressions: [], + expected: code, + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + const expressionParserMock = new ExpressionParserStub() + .withResult(code, testCase.expressions); + const args = new FunctionCallArgumentCollectionStub(); + const sut = new SystemUnderTest(expressionParserMock); + // act + const actual = sut.compileExpressions(code, args); + // assert + expect(actual).to.equal(testCase.expected); + }); + } + }); + describe('arguments', () => { + it('passes arguments to expressions as expected', () => { + // arrange + const expected = new FunctionCallArgumentCollectionStub() + .withArgument('test-arg', 'test-value'); + const code = 'longer than 6 characters'; + const expressions = [ + new ExpressionStub().withPosition(0, 3), + new ExpressionStub().withPosition(3, 6), + ]; + const expressionParserMock = new ExpressionParserStub() + .withResult(code, expressions); + const sut = new SystemUnderTest(expressionParserMock); + // act + sut.compileExpressions(code, expected); + // assert + const actualArgs = expressions + .flatMap((expression) => expression.callHistory) + .map((context) => context.args); + expect( + actualArgs.every((arg) => arg === expected), + `Expected: ${JSON.stringify(expected)}\n` + + `Actual: ${JSON.stringify(actualArgs)}\n` + + `Not equal: ${actualArgs.filter((arg) => arg !== expected)}`, + ); + }); + }); + describe('throws when expressions are invalid', () => { + describe('throws when expected argument is not provided but used in code', () => { + // arrange + const testCases = [ + { + name: 'empty parameters', + expressions: [ + new ExpressionStub().withParameterNames(['parameter'], false), + ], + args: new FunctionCallArgumentCollectionStub(), + expectedError: 'parameter value(s) not provided for: "parameter" but used in code', + }, + { + name: 'unnecessary parameter is provided', + expressions: [ + new ExpressionStub().withParameterNames(['parameter'], false), + ], + args: new FunctionCallArgumentCollectionStub() + .withArgument('unnecessaryParameter', 'unnecessaryValue'), + expectedError: 'parameter value(s) not provided for: "parameter" but used in code', + }, + { + name: 'multiple values are not provided', + expressions: [ + new ExpressionStub().withParameterNames(['parameter1'], false), + new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false), + ], + args: new FunctionCallArgumentCollectionStub(), + expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3" but used in code', + }, + { + name: 'some values are provided', + expressions: [ + new ExpressionStub().withParameterNames(['parameter1'], false), + new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false), + ], + args: new FunctionCallArgumentCollectionStub() + .withArgument('parameter2', 'value'), + expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3" but used in code', + }, + { + name: 'parameter names are not repeated in error message', + expressions: [ + new ExpressionStub().withParameterNames(['parameter1', 'parameter1', 'parameter2', 'parameter2'], false), + ], + args: new FunctionCallArgumentCollectionStub(), + expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2" but used in code', + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + const code = 'non-important-code'; + const expressionParserMock = new ExpressionParserStub() + .withResult(code, testCase.expressions); + const sut = new SystemUnderTest(expressionParserMock); + // act + const act = () => sut.compileExpressions(code, testCase.args); + // assert + expect(act).to.throw(testCase.expectedError); + }); + } + }); + describe('throws when expression positions are unexpected', () => { + // arrange + const code = 'c'.repeat(30); + const testCases: readonly { + name: string, + expressions: readonly IExpression[], + expectedError: string, + expectedResult: boolean, + }[] = [ + (() => { + const badExpression = new ExpressionStub().withPosition(0, code.length + 5); + const goodExpression = new ExpressionStub().withPosition(0, code.length - 1); + return { + name: 'an expression has out-of-range position', + expressions: [badExpression, goodExpression], + expectedError: `Expressions out of range:\n${JSON.stringify([badExpression])}`, + expectedResult: true, + }; + })(), + (() => { + const duplicatedExpression = new ExpressionStub().withPosition(0, code.length - 1); + const uniqueExpression = new ExpressionStub().withPosition(0, code.length - 2); + return { + name: 'two expressions at the same position', + expressions: [duplicatedExpression, duplicatedExpression, uniqueExpression], + expectedError: `Instructions at same position:\n${JSON.stringify([duplicatedExpression, duplicatedExpression])}`, + expectedResult: true, + }; + })(), + (() => { + const goodExpression = new ExpressionStub().withPosition(0, 5); + const intersectingExpression = new ExpressionStub().withPosition(5, 10); + const intersectingExpressionOther = new ExpressionStub().withPosition(7, 12); + return { + name: 'intersecting expressions', + expressions: [goodExpression, intersectingExpression, intersectingExpressionOther], + expectedError: `Instructions intersecting unexpectedly:\n${JSON.stringify([intersectingExpression, intersectingExpressionOther])}`, + expectedResult: true, + }; + })(), + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + const expressionParserMock = new ExpressionParserStub() + .withResult(code, testCase.expressions); + const sut = new SystemUnderTest(expressionParserMock); + const args = new FunctionCallArgumentCollectionStub(); + // act + const act = () => sut.compileExpressions(code, args); + // assert + expect(act).to.throw(testCase.expectedError); + }); + } + }); + }); + it('calls parser with expected code', () => { + // arrange + const expected = 'expected-code'; + const expressionParserMock = new ExpressionParserStub(); + const sut = new SystemUnderTest(expressionParserMock); + const args = new FunctionCallArgumentCollectionStub(); + // act + sut.compileExpressions(expected, args); + // assert + expect(expressionParserMock.callHistory).to.have.lengthOf(1); + expect(expressionParserMock.callHistory[0]).to.equal(expected); + }); + }); +}); + +class SystemUnderTest extends ExpressionsCompiler { + constructor(extractor: IExpressionParser = new ExpressionParserStub()) { + super(extractor); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.spec.ts new file mode 100644 index 00000000..b978d7d1 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.spec.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from 'vitest'; +import type { IExpression } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/IExpression'; +import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser'; +import { CompositeExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser'; +import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub'; +import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('CompositeExpressionParser', () => { + describe('ctor', () => { + describe('throws when parsers are missing', () => { + itEachAbsentCollectionValue((absentCollection) => { + // arrange + const expectedError = 'missing leafs'; + const parsers = absentCollection; + // act + const act = () => new CompositeExpressionParser(parsers); + // assert + expect(act).to.throw(expectedError); + }, { excludeUndefined: true, excludeNull: true }); + }); + }); + describe('findExpressions', () => { + describe('returns result from parsers as expected', () => { + // arrange + const pool = [ + new ExpressionStub(), new ExpressionStub(), new ExpressionStub(), + new ExpressionStub(), new ExpressionStub(), + ]; + const testCases = [ + { + name: 'from single parsing none', + parsers: [mockParser()], + expected: [], + }, + { + name: 'from single parsing single', + parsers: [mockParser(pool[0])], + expected: [pool[0]], + }, + { + name: 'from single parsing multiple', + parsers: [mockParser(pool[0], pool[1])], + expected: [pool[0], pool[1]], + }, + { + name: 'from multiple parsers with each parsing single', + parsers: [ + mockParser(pool[0]), + mockParser(pool[1]), + mockParser(pool[2]), + ], + expected: [pool[0], pool[1], pool[2]], + }, + { + name: 'from multiple parsers with each parsing multiple', + parsers: [ + mockParser(pool[0], pool[1]), + mockParser(pool[2], pool[3], pool[4])], + expected: [pool[0], pool[1], pool[2], pool[3], pool[4]], + }, + { + name: 'from multiple parsers with only some parsing', + parsers: [ + mockParser(pool[0], pool[1]), + mockParser(), + mockParser(pool[2]), + mockParser(), + ], + expected: [pool[0], pool[1], pool[2]], + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + const sut = new CompositeExpressionParser(testCase.parsers); + // act + const result = sut.findExpressions('non-important-code'); + // expect + expect(result).to.deep.equal(testCase.expected); + }); + } + }); + }); +}); + +function mockParser(...result: IExpression[]): IExpressionParser { + return { + findExpressions: () => result, + }; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.spec.ts new file mode 100644 index 00000000..40882633 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.spec.ts @@ -0,0 +1,428 @@ +import { describe, it, expect } from 'vitest'; +import { ExpressionRegexBuilder } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder'; + +const AllWhitespaceCharacters = ' \t\n\r\v\f\u00A0'; + +describe('ExpressionRegexBuilder', () => { + describe('expectCharacters', () => { + describe('expectCharacters', () => { + describe('escapes single character as expected', () => { + const charactersToEscape = ['.', '$']; + for (const character of charactersToEscape) { + it(`escapes ${character} as expected`, () => expectMatch( + character, + (act) => act.expectCharacters(character), + `${character}`, + )); + } + }); + it('escapes multiple characters as expected', () => expectMatch( + '.I have no $$.', + (act) => act.expectCharacters('.I have no $$.'), + '.I have no $$.', + )); + it('adds characters as expected', () => expectMatch( + 'return as it is', + (act) => act.expectCharacters('return as it is'), + 'return as it is', + )); + }); + }); + describe('expectOneOrMoreWhitespaces', () => { + it('matches one whitespace', () => expectMatch( + ' ', + (act) => act.expectOneOrMoreWhitespaces(), + ' ', + )); + it('matches multiple whitespaces', () => expectMatch( + AllWhitespaceCharacters, + (act) => act.expectOneOrMoreWhitespaces(), + AllWhitespaceCharacters, + )); + it('matches whitespaces inside text', () => expectMatch( + `start${AllWhitespaceCharacters}end`, + (act) => act.expectOneOrMoreWhitespaces(), + AllWhitespaceCharacters, + )); + it('does not match non-whitespace characters', () => expectNonMatch( + 'a', + (act) => act.expectOneOrMoreWhitespaces(), + )); + }); + describe('captureOptionalPipeline', () => { + it('does not capture when no pipe is present', () => expectNonMatch( + 'noPipeHere', + (act) => act.captureOptionalPipeline(), + )); + it('captures when input starts with pipe', () => expectCapture( + '| afterPipe', + (act) => act.captureOptionalPipeline(), + '| afterPipe', + )); + it('ignores without text before', () => expectCapture( + 'stuff before | afterPipe', + (act) => act.captureOptionalPipeline(), + '| afterPipe', + )); + it('ignores without text before', () => expectCapture( + 'stuff before | afterPipe', + (act) => act.captureOptionalPipeline(), + '| afterPipe', + )); + it('ignores whitespaces before the pipe', () => expectCapture( + ' | afterPipe', + (act) => act.captureOptionalPipeline(), + '| afterPipe', + )); + it('ignores text after whitespace', () => expectCapture( + '| first Pipe', + (act) => act.captureOptionalPipeline(), + '| first ', + )); + describe('non-greedy matching', () => { // so the rest of the pattern can work + it('non-letter character in pipe', () => expectCapture( + '| firstPipe | sec0ndpipe', + (act) => act.captureOptionalPipeline(), + '| firstPipe ', + )); + }); + }); + describe('captureUntilWhitespaceOrPipe', () => { + it('captures until first whitespace', () => expectCapture( + // arrange + 'first ', + // act + (act) => act.captureUntilWhitespaceOrPipe(), + // assert + 'first', + )); + it('captures until first pipe', () => expectCapture( + // arrange + 'first|', + // act + (act) => act.captureUntilWhitespaceOrPipe(), + // assert + 'first', + )); + it('captures all without whitespace or pipe', () => expectCapture( + // arrange + 'all', + // act + (act) => act.captureUntilWhitespaceOrPipe(), + // assert + 'all', + )); + }); + describe('captureMultilineAnythingExceptSurroundingWhitespaces', () => { + describe('single line', () => { + it('captures a line without surrounding whitespaces', () => expectCapture( + // arrange + 'line', + // act + (act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(), + // assert + 'line', + )); + it('captures a line with internal whitespaces intact', () => expectCapture( + `start${AllWhitespaceCharacters}end`, + (act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(), + `start${AllWhitespaceCharacters}end`, + )); + it('excludes surrounding whitespaces', () => expectCapture( + // arrange + `${AllWhitespaceCharacters}single line\t`, + // act + (act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(), + // assert + 'single line', + )); + }); + describe('multiple lines', () => { + it('captures text across multiple lines', () => expectCapture( + // arrange + 'first line\nsecond line\r\nthird-line', + // act + (act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(), + // assert + 'first line\nsecond line\r\nthird-line', + )); + it('captures text with empty lines in between', () => expectCapture( + 'start\n\nend', + (act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(), + 'start\n\nend', + )); + it('excludes surrounding whitespaces from multiline text', () => expectCapture( + // arrange + ` first line\nsecond line${AllWhitespaceCharacters}`, + // act + (act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(), + // assert + 'first line\nsecond line', + )); + }); + describe('edge cases', () => { + it('does not capture for input with only whitespaces', () => expectNonCapture( + AllWhitespaceCharacters, + (act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(), + )); + }); + }); + describe('expectExpressionStart', () => { + it('matches expression start without trailing whitespaces', () => expectMatch( + '{{expression', + (act) => act.expectExpressionStart(), + '{{', + )); + it('matches expression start with trailing whitespaces', () => expectMatch( + `{{${AllWhitespaceCharacters}expression`, + (act) => act.expectExpressionStart(), + `{{${AllWhitespaceCharacters}`, + )); + it('does not match whitespaces not directly after expression start', () => expectMatch( + ' {{expression', + (act) => act.expectExpressionStart(), + '{{', + )); + it('does not match if expression start is not present', () => expectNonMatch( + 'noExpressionStartHere', + (act) => act.expectExpressionStart(), + )); + }); + describe('expectExpressionEnd', () => { + it('matches expression end without preceding whitespaces', () => expectMatch( + 'expression}}', + (act) => act.expectExpressionEnd(), + '}}', + )); + it('matches expression end with preceding whitespaces', () => expectMatch( + `expression${AllWhitespaceCharacters}}}`, + (act) => act.expectExpressionEnd(), + `${AllWhitespaceCharacters}}}`, + )); + it('does not capture whitespaces not directly before expression end', () => expectMatch( + 'expression}} ', + (act) => act.expectExpressionEnd(), + '}}', + )); + it('does not match if expression end is not present', () => expectNonMatch( + 'noExpressionEndHere', + (act) => act.expectExpressionEnd(), + )); + }); + describe('expectOptionalWhitespaces', () => { + describe('matching', () => { + it('matches multiple Unix lines', () => expectMatch( + // arrange + '\n\n', + // act + (act) => act.expectOptionalWhitespaces(), + // assert + '\n\n', + )); + it('matches multiple Windows lines', () => expectMatch( + // arrange + '\r\n', + // act + (act) => act.expectOptionalWhitespaces(), + // assert + '\r\n', + )); + it('matches multiple spaces', () => expectMatch( + // arrange + ' ', + // act + (act) => act.expectOptionalWhitespaces(), + // assert + ' ', + )); + it('matches horizontal and vertical tabs', () => expectMatch( + // arrange + '\t\v', + // act + (act) => act.expectOptionalWhitespaces(), + // assert + '\t\v', + )); + it('matches form feed character', () => expectMatch( + // arrange + '\f', + // act + (act) => act.expectOptionalWhitespaces(), + // assert + '\f', + )); + it('matches a non-breaking space character', () => expectMatch( + // arrange + '\u00A0', + // act + (act) => act.expectOptionalWhitespaces(), + // assert + '\u00A0', + )); + it('matches a combination of whitespace characters', () => expectMatch( + // arrange + AllWhitespaceCharacters, + // act + (act) => act.expectOptionalWhitespaces(), + // assert + AllWhitespaceCharacters, + )); + it('matches whitespace characters on different positions', () => expectMatch( + // arrange + '\ta\nb\rc\v', + // act + (act) => act.expectOptionalWhitespaces(), + // assert + '\t\n\r\v', + )); + }); + describe('non-matching', () => { + it('a non-whitespace character', () => expectNonMatch( + // arrange + 'a', + // act + (act) => act.expectOptionalWhitespaces(), + )); + it('multiple non-whitespace characters', () => expectNonMatch( + // arrange + 'abc', + // act + (act) => act.expectOptionalWhitespaces(), + )); + }); + }); + describe('buildRegExp', () => { + it('sets global flag', () => { + // arrange + const expected = 'g'; + const sut = new ExpressionRegexBuilder() + .expectOneOrMoreWhitespaces(); + // act + const actual = sut.buildRegExp().flags; + // assert + expect(actual).to.equal(expected); + }); + describe('can combine multiple parts', () => { + it('combines character and whitespace expectations', () => expectMatch( + 'abc def', + (act) => act + .expectCharacters('abc') + .expectOneOrMoreWhitespaces() + .expectCharacters('def'), + 'abc def', + )); + it('captures optional pipeline and text after it', () => expectCapture( + 'abc | def', + (act) => act + .expectCharacters('abc ') + .captureOptionalPipeline(), + '| def', + )); + it('combines multiline capture with optional whitespaces', () => expectCapture( + '\n abc \n', + (act) => act + .expectOptionalWhitespaces() + .captureMultilineAnythingExceptSurroundingWhitespaces() + .expectOptionalWhitespaces(), + 'abc', + )); + it('combines expression start, optional whitespaces, and character expectation', () => expectMatch( + '{{ abc', + (act) => act + .expectExpressionStart() + .expectOptionalWhitespaces() + .expectCharacters('abc'), + '{{ abc', + )); + it('combines character expectation, optional whitespaces, and expression end', () => expectMatch( + 'abc }}', + (act) => act + .expectCharacters('abc') + .expectOptionalWhitespaces() + .expectExpressionEnd(), + 'abc }}', + )); + }); + }); +}); + +enum MatchGroupIndex { + FullMatch = 0, + FirstCapturingGroup = 1, +} + +function expectCapture( + input: string, + act: (regexBuilder: ExpressionRegexBuilder) => ExpressionRegexBuilder, + expectedCombinedCaptures: string | undefined, +): void { + // arrange + const matchGroupIndex = MatchGroupIndex.FirstCapturingGroup; + // act + // assert + expectMatch(input, act, expectedCombinedCaptures, matchGroupIndex); +} + +function expectNonMatch( + input: string, + act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder, + matchGroupIndex = MatchGroupIndex.FullMatch, +): void { + expectMatch(input, act, undefined, matchGroupIndex); +} + +function expectNonCapture( + input: string, + act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder, +): void { + expectNonMatch(input, act, MatchGroupIndex.FirstCapturingGroup); +} + +function expectMatch( + input: string, + act: (regexBuilder: ExpressionRegexBuilder) => ExpressionRegexBuilder, + expectedCombinedMatches: string | undefined, + matchGroupIndex = MatchGroupIndex.FullMatch, +): void { + // arrange + const regexBuilder = new ExpressionRegexBuilder(); + act(regexBuilder); + const regex = regexBuilder.buildRegExp(); + // act + const allMatchGroups = Array.from(input.matchAll(regex)); + // assert + const actualMatches = allMatchGroups + .filter((matches) => matches.length > matchGroupIndex) + .map((matches) => matches[matchGroupIndex]) + .filter(Boolean) // matchAll returns `""` for full matches, `null` for capture groups + .flat(); + const actualCombinedMatches = actualMatches.length ? actualMatches.join('') : undefined; + expect(actualCombinedMatches).equal( + expectedCombinedMatches, + [ + '\n\n---', + 'Expected combined matches:', + getTestDataText(expectedCombinedMatches), + 'Actual combined matches:', + getTestDataText(actualCombinedMatches), + 'Input:', + getTestDataText(input), + 'Regex:', + getTestDataText(regex.toString()), + 'All match groups:', + getTestDataText(JSON.stringify(allMatchGroups)), + `Match index in group: ${matchGroupIndex}`, + '---\n\n', + ].join('\n'), + ); +} + +function getTestDataText(data: string | undefined): string { + const outputPrefix = '\t> '; + if (data === undefined) { + return `${outputPrefix}undefined (no matches)`; + } + const getLiteralString = (text: string) => JSON.stringify(text).slice(1, -1); + const text = `${outputPrefix}\`${getLiteralString(data)}\``; + return text; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.spec.ts new file mode 100644 index 00000000..ce2f28d0 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.spec.ts @@ -0,0 +1,438 @@ +import { describe, it, expect } from 'vitest'; +import type { + ExpressionEvaluator, ExpressionInitParameters, +} from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/Expression'; +import { + type PrimitiveExpression, RegexParser, type ExpressionFactory, type RegexParserUtilities, +} from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser'; +import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector'; +import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; +import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub'; +import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub'; +import type { IExpression } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/IExpression'; +import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub'; +import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection'; +import type { ExpressionPositionFactory } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPositionFactory'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory'; +import { indentText } from '@/application/Common/Text/IndentText'; + +describe('RegexParser', () => { + describe('findExpressions', () => { + describe('error handling', () => { + describe('throws when code is absent', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing code'; + const sut = new RegexParserConcrete({ + regex: /unimportant/, + }); + // act + const act = () => sut.findExpressions(absentValue); + // assert + const errorMessage = collectExceptionMessage(act); + expect(errorMessage).to.include(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + describe('rethrows regex match errors', () => { + // arrange + const expectedMatchError = new TypeError('String.prototype.matchAll called with a non-global RegExp argument'); + const expectedMessage = 'Failed to match regex.'; + const expectedCodeInMessage = 'unimportant code content'; + const expectedRegexInMessage = /failing-regex-because-it-is-non-global/; + const expectedErrorMessage = buildRethrowErrorMessage({ + message: expectedMessage, + code: expectedCodeInMessage, + regex: expectedRegexInMessage, + }); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + const sut = new RegexParserConcrete( + { + regex: expectedRegexInMessage, + utilities: { + wrapError, + }, + }, + ); + sut.findExpressions(expectedCodeInMessage); + }, + // assert + expectedContextMessage: expectedErrorMessage, + expectedWrappedError: expectedMatchError, + }); + }); + describe('rethrows expression building errors', () => { + // arrange + const expectedMessage = 'Failed to build expression.'; + const expectedInnerError = new Error('Expected error from building expression'); + const { + code: expectedCodeInMessage, + regex: expectedRegexInMessage, + } = createCodeAndRegexMatchingOnce(); + const throwingExpressionBuilder = () => { + throw expectedInnerError; + }; + const expectedErrorMessage = buildRethrowErrorMessage({ + message: expectedMessage, + code: expectedCodeInMessage, + regex: expectedRegexInMessage, + }); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + const sut = new RegexParserConcrete( + { + regex: expectedRegexInMessage, + builder: throwingExpressionBuilder, + utilities: { + wrapError, + }, + }, + ); + sut.findExpressions(expectedCodeInMessage); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + describe('rethrows position creation errors', () => { + // arrange + const expectedMessage = 'Failed to create position.'; + const expectedInnerError = new Error('Expected error from position factory'); + const { + code: expectedCodeInMessage, + regex: expectedRegexInMessage, + } = createCodeAndRegexMatchingOnce(); + const throwingPositionFactory = () => { + throw expectedInnerError; + }; + const expectedErrorMessage = buildRethrowErrorMessage({ + message: expectedMessage, + code: expectedCodeInMessage, + regex: expectedRegexInMessage, + }); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + const sut = new RegexParserConcrete( + { + regex: expectedRegexInMessage, + utilities: { + createPosition: throwingPositionFactory, + wrapError, + }, + }, + ); + sut.findExpressions(expectedCodeInMessage); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + describe('rethrows parameter creation errors', () => { + // arrange + const expectedMessage = 'Failed to create parameters.'; + const expectedInnerError = new Error('Expected error from parameter collection factory'); + const { + code: expectedCodeInMessage, + regex: expectedRegexInMessage, + } = createCodeAndRegexMatchingOnce(); + const throwingParameterCollectionFactory = () => { + throw expectedInnerError; + }; + const expectedErrorMessage = buildRethrowErrorMessage({ + message: expectedMessage, + code: expectedCodeInMessage, + regex: expectedRegexInMessage, + }); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + const sut = new RegexParserConcrete( + { + regex: expectedRegexInMessage, + utilities: { + createParameterCollection: throwingParameterCollectionFactory, + wrapError, + }, + }, + ); + sut.findExpressions(expectedCodeInMessage); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + describe('rethrows expression creation errors', () => { + // arrange + const expectedMessage = 'Failed to create expression.'; + const expectedInnerError = new Error('Expected error from expression factory'); + const { + code: expectedCodeInMessage, + regex: expectedRegexInMessage, + } = createCodeAndRegexMatchingOnce(); + const throwingExpressionFactory = () => { + throw expectedInnerError; + }; + const expectedErrorMessage = buildRethrowErrorMessage({ + message: expectedMessage, + code: expectedCodeInMessage, + regex: expectedRegexInMessage, + }); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + const sut = new RegexParserConcrete( + { + regex: expectedRegexInMessage, + utilities: { + createExpression: throwingExpressionFactory, + wrapError, + }, + }, + ); + sut.findExpressions(expectedCodeInMessage); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + }); + describe('handles matched regex correctly', () => { + // arrange + const testScenarios: readonly { + readonly description: string; + readonly regex: RegExp; + readonly code: string; + }[] = [ + { + description: 'non-matching regex', + regex: /hello/g, + code: 'world', + }, + { + description: 'single regex match', + regex: /hello/g, + code: 'hello world', + }, + { + description: 'multiple regex matches', + regex: /l/g, + code: 'hello world', + }, + ]; + testScenarios.forEach(({ + description, code, regex, + }) => { + describe(description, () => { + it('generates expressions for all matches', () => { + // arrange + const expectedTotalExpressions = Array.from(code.matchAll(regex)).length; + const sut = new RegexParserConcrete({ + regex, + }); + // act + const expressions = sut.findExpressions(code); + // assert + const actualTotalExpressions = expressions.length; + expect(actualTotalExpressions).to.equal( + expectedTotalExpressions, + formatAssertionMessage([ + `Expected ${actualTotalExpressions} expressions due to ${expectedTotalExpressions} matches`, + `Expressions:\n${indentText(JSON.stringify(expressions, undefined, 2))}`, + ]), + ); + }); + it('builds primitive expressions for each match', () => { + const expected = Array.from(code.matchAll(regex)); + const matches = new Array(); + const builder = (m: RegExpMatchArray): PrimitiveExpression => { + matches.push(m); + return createPrimitiveExpressionStub(); + }; + const sut = new RegexParserConcrete({ + regex, + builder, + }); + // act + sut.findExpressions(code); + // assert + expect(matches).to.deep.equal(expected); + }); + it('sets positions correctly from matches', () => { + // arrange + const expectedMatches = [...code.matchAll(regex)]; + const { createExpression, getInitParameters } = createExpressionFactorySpy(); + const serializeRegexMatch = (match: RegExpMatchArray) => `[startPos:${match?.index ?? 'none'},length:${match?.[0]?.length ?? 'none'}]`; + const positionsForMatches = new Map(expectedMatches.map( + (expectedMatch) => [serializeRegexMatch(expectedMatch), new ExpressionPosition(1, 4)], + )); + const createPositionMock: ExpressionPositionFactory = (match) => { + const position = positionsForMatches.get(serializeRegexMatch(match)); + return position ?? new ExpressionPosition(66, 666); + }; + const sut = new RegexParserConcrete({ + regex, + utilities: { + createExpression, + createPosition: createPositionMock, + }, + }); + // act + const expressions = sut.findExpressions(code); + // assert + const expectedPositions = [...positionsForMatches.values()]; + const actualPositions = expressions.map((e) => getInitParameters(e)?.position); + expect(actualPositions).to.deep.equal(expectedPositions, formatAssertionMessage([ + 'Actual positions do not match the expected positions.', + `Expected total positions: ${expectedPositions.length} (due to ${expectedMatches.length} regex matches)`, + `Actual total positions: ${actualPositions.length}`, + `Expected positions:\n${indentText(JSON.stringify(expectedPositions, undefined, 2))}`, + `Actual positions:\n${indentText(JSON.stringify(actualPositions, undefined, 2))}`, + ])); + }); + }); + }); + }); + it('sets evaluator correctly from expression', () => { + // arrange + const { createExpression, getInitParameters } = createExpressionFactorySpy(); + const expectedEvaluate = createEvaluatorStub(); + const { code, regex } = createCodeAndRegexMatchingOnce(); + const builder = (): PrimitiveExpression => ({ + evaluator: expectedEvaluate, + }); + const sut = new RegexParserConcrete({ + regex, + builder, + utilities: { + createExpression, + }, + }); + // act + const expressions = sut.findExpressions(code); + // assert + expect(expressions).to.have.lengthOf(1); + const actualEvaluate = getInitParameters(expressions[0])?.evaluator; + expect(actualEvaluate).to.equal(expectedEvaluate); + }); + it('sets parameters correctly from expression', () => { + // arrange + const expectedParameters: IReadOnlyFunctionParameterCollection['all'] = [ + new FunctionParameterStub().withName('parameter1').withOptional(true), + new FunctionParameterStub().withName('parameter2').withOptional(false), + ]; + const regex = /hello/g; + const code = 'hello'; + const builder = (): PrimitiveExpression => ({ + evaluator: createEvaluatorStub(), + parameters: expectedParameters, + }); + const parameterCollection = new FunctionParameterCollectionStub(); + const parameterCollectionFactoryStub + : FunctionParameterCollectionFactory = () => parameterCollection; + const { createExpression, getInitParameters } = createExpressionFactorySpy(); + const sut = new RegexParserConcrete({ + regex, + builder, + utilities: { + createExpression, + createParameterCollection: parameterCollectionFactoryStub, + }, + }); + // act + const expressions = sut.findExpressions(code); + // assert + expect(expressions).to.have.lengthOf(1); + const actualParameters = getInitParameters(expressions[0])?.parameters; + expect(actualParameters).to.equal(parameterCollection); + expect(actualParameters?.all).to.deep.equal(expectedParameters); + }); + }); +}); + +function buildRethrowErrorMessage( + expectedContext: { + readonly message: string; + readonly regex: RegExp; + readonly code: string; + }, +): string { + return [ + expectedContext.message, + `Class name: ${RegexParserConcrete.name}`, + `Regex pattern used: ${expectedContext.regex}`, + `Code: ${expectedContext.code}`, + ].join('\n'); +} + +function createExpressionFactorySpy() { + const createdExpressions = new Map(); + const createExpression: ExpressionFactory = (parameters) => { + const expression = new ExpressionStub(); + createdExpressions.set(expression, parameters); + return expression; + }; + return { + createExpression, + getInitParameters: (expression) => createdExpressions.get(expression), + }; +} + +function createBuilderStub(): (match: RegExpMatchArray) => PrimitiveExpression { + return () => ({ + evaluator: createEvaluatorStub(), + }); +} +function createEvaluatorStub(): ExpressionEvaluator { + return () => `[${createEvaluatorStub.name}] evaluated code`; +} + +function createPrimitiveExpressionStub(): PrimitiveExpression { + return { + evaluator: createEvaluatorStub(), + }; +} + +function createCodeAndRegexMatchingOnce() { + const code = 'expected code in context'; + const regex = /code/g; + return { code, regex }; +} + +class RegexParserConcrete extends RegexParser { + private readonly builder: RegexParser['buildExpression']; + + protected regex: RegExp; + + public constructor(parameters?: { + regex?: RegExp, + builder?: RegexParser['buildExpression'], + utilities?: Partial, + }) { + super({ + wrapError: parameters?.utilities?.wrapError + ?? (() => new Error(`[${RegexParserConcrete}] wrapped error`)), + createPosition: parameters?.utilities?.createPosition + ?? (() => new ExpressionPosition(0, 5)), + createExpression: parameters?.utilities?.createExpression + ?? (() => new ExpressionStub()), + createParameterCollection: parameters?.utilities?.createParameterCollection + ?? (() => new FunctionParameterCollectionStub()), + }); + this.builder = parameters?.builder ?? createBuilderStub(); + this.regex = parameters?.regex ?? /unimportant/g; + } + + protected buildExpression(match: RegExpMatchArray): PrimitiveExpression { + return this.builder(match); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/EscapeDoubleQuotes.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/EscapeDoubleQuotes.spec.ts new file mode 100644 index 00000000..314f197d --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/EscapeDoubleQuotes.spec.ts @@ -0,0 +1,33 @@ +import { describe } from 'vitest'; +import { EscapeDoubleQuotes } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/EscapeDoubleQuotes'; +import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; +import { runPipeTests, type PipeTestScenario } from './PipeTestRunner'; + +describe('EscapeDoubleQuotes', () => { + // arrange + const sut = new EscapeDoubleQuotes(); + // act + runPipeTests(sut, [ + ...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true }) + .map((testCase): PipeTestScenario => ({ + description: `returns empty when if input is missing (${testCase.valueName})`, + input: testCase.absentValue, + expectedOutput: '', + })), + { + description: 'using "', + input: 'hello "world"', + expectedOutput: 'hello "^""world"^""', + }, + { + description: 'not using any double quotes', + input: 'hello world', + expectedOutput: 'hello world', + }, + { + description: 'consecutive double quotes', + input: '""hello world""', + expectedOutput: '"^"""^""hello world"^"""^""', + }, + ]); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.spec.ts new file mode 100644 index 00000000..070f7fce --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.spec.ts @@ -0,0 +1,50 @@ +import { describe } from 'vitest'; +import { InlinePowerShell } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell'; +import { runPipeTests, type PipeTestScenario } from './PipeTestRunner'; +import { createTryCatchFinallyTests } from './InlinePowerShellTests/CreateTryCatchFinallyTests'; +import { createAbsentCodeTests } from './InlinePowerShellTests/CreateAbsentCodeTests'; +import { createCommentedCodeTests } from './InlinePowerShellTests/CreateCommentedCodeTests'; +import { createIfStatementTests } from './InlinePowerShellTests/CreateIfStatementTests'; +import { createLineContinuationBacktickCases } from './InlinePowerShellTests/CreateLineContinuationBacktickTests'; +import { createDoWhileTests } from './InlinePowerShellTests/CreateDoWhileTests'; +import { createDoUntilTests } from './InlinePowerShellTests/CreateDoUntilTests'; +import { createForeachTests } from './InlinePowerShellTests/CreateForeachTests'; +import { createWhileTests } from './InlinePowerShellTests/CreateWhileTests'; +import { createForLoopTests } from './InlinePowerShellTests/CreateForLoopTests'; +import { createSwitchTests } from './InlinePowerShellTests/CreateSwitchTests'; +import { createHereStringTests } from './InlinePowerShellTests/CreateHereStringTests'; +import { createNewlineTests } from './InlinePowerShellTests/CreateNewlineTests'; +import { createFunctionTests } from './InlinePowerShellTests/CreateFunctionTests'; +import { createScriptBlockTests } from './InlinePowerShellTests/CreateScriptBlockTests'; + +describe('InlinePowerShell', () => { + // arrange + const sut = new InlinePowerShell(); + // act + runPipeTests(sut, [ + ...prefixTests('absent code', createAbsentCodeTests()), + ...prefixTests('newline', createNewlineTests()), + ...prefixTests('comment', createCommentedCodeTests()), + ...prefixTests('here-string', createHereStringTests()), + ...prefixTests('line continuation backtick', createLineContinuationBacktickCases()), + ...prefixTests('try-catch-finally', createTryCatchFinallyTests()), + ...prefixTests('if statement', createIfStatementTests()), + ...prefixTests('do-while loop', createDoWhileTests()), + ...prefixTests('do-until loop', createDoUntilTests()), + ...prefixTests('foreach loop', createForeachTests()), + ...prefixTests('while loop', createWhileTests()), + ...prefixTests('for loop', createForLoopTests()), + ...prefixTests('switch statement', createSwitchTests()), + ...prefixTests('function', createFunctionTests()), + ...prefixTests('script block', createScriptBlockTests()), + ]); +}); + +function prefixTests(prefix: string, tests: PipeTestScenario[]): PipeTestScenario[] { + return tests.map((test) => ({ + ...test, + ...{ + description: `[${prefix}] ${test.description}`, + }, + })); +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CommonInlinePowerShellTestUtilities.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CommonInlinePowerShellTestUtilities.ts new file mode 100644 index 00000000..d92fd4dd --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CommonInlinePowerShellTestUtilities.ts @@ -0,0 +1,26 @@ +import { RegexBuilder } from '../PipeTestRunner'; + +export function joinAsWindowsLines( + ...lines: string[] +): string { + return lines.join('\r\n'); +} + +/** + * Builds a relaxed regular expression pattern for matching inlined multiple lines of code + * with basic semicolon merging. + */ +export function getInlinedOutputWithSemicolons( + ...lines: string[] +): RegExp { + const trimmedLines = lines.map((line) => line.trim()); + const builder = new RegexBuilder(); + trimmedLines.forEach((line, index) => { + builder.withLiteralString(line); + builder.withOptionalSemicolon(); // Semi colon at the end compiles fine + if (index !== trimmedLines.length - 1) { + builder.withOptionalWhitespaceButNoNewline(); + } + }); + return builder.buildRegex(); +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateAbsentCodeTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateAbsentCodeTests.ts new file mode 100644 index 00000000..d7d6759b --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateAbsentCodeTests.ts @@ -0,0 +1,28 @@ +import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; +import type { PipeTestScenario } from '../PipeTestRunner'; + +export function createAbsentCodeTests(): PipeTestScenario[] { + return [ + ...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true }) + .map((testCase): PipeTestScenario => ({ + description: `absent string (${testCase.valueName})`, + input: testCase.absentValue, + expectedOutput: '', + })), + { + description: 'whitespace-only input', + input: ' \t\n\r\f\v\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000', + expectedOutput: '', + }, + { + description: 'newline-only input', + input: '\n\r\u2028\u2029', + expectedOutput: '', + }, + { + description: 'newline-only input', + input: ' \t\n\r\f\v\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\n\r\u2028\u2029', + expectedOutput: '', + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateCommentedCodeTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateCommentedCodeTests.ts new file mode 100644 index 00000000..7ff0c8a8 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateCommentedCodeTests.ts @@ -0,0 +1,122 @@ +import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; +import type { PipeTestScenario } from '../PipeTestRunner'; + +export function createCommentedCodeTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-7.4 + return [ + { + description: 'converts hash comments at line end', + input: joinAsWindowsLines( + '$text = "Hello"\t# Comment after tab', + '$text+= #Comment without space after hash', + 'Write-Host $text# Comment without space before hash', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '$text = "Hello"\t<# Comment after tab #>', + '$text+= <# Comment without space after hash #>', + 'Write-Host $text<# Comment without space before hash #>', + ), + }, + { + description: 'converts hash comment lines', + input: joinAsWindowsLines( + '# Comment in first line', + 'Write-Host "Hello"', + '# Comment in the middle', + 'Write-Host "World"', + '# Consecutive comments', + '# Last line comment without line ending in the end', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '<# Comment in first line #>', + 'Write-Host "Hello"', + '<# Comment in the middle #>', + 'Write-Host "World"', + '<# Consecutive comments #>', + '<# Last line comment without line ending in the end #>', + ), + }, + { + description: 'converts comments with inline comment parts inside', + input: joinAsWindowsLines( + '$text+= #Comment with < inside', + '$text+= #Comment ending with >', + '$text+= #Comment with <# inline comment #>', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '$text+= <# Comment with < inside #>', + '$text+= <# Comment ending with > #>', + '$text+= <# Comment with <# inline comment #> #>', + ), + }, + { + description: 'converts comments with inline comment parts around', // Pretty uncommon + input: joinAsWindowsLines( + 'Write-Host "hi" # Comment ending line inline comment but not one #>', + 'Write-Host "hi" #>Comment starting like inline comment end but not one', + // Following line does not compile as valid PowerShell due to missing #> for inline comment. + 'Write-Host "hi" <#Comment starting like inline comment start but not one', + ), + expectedOutput: getInlinedOutputWithSemicolons( + 'Write-Host "hi" <# Comment ending line inline comment but not one #> #>', + 'Write-Host "hi" <# >Comment starting like inline comment end but not one #>', + 'Write-Host "hi" <<# Comment starting like inline comment start but not one #>', + ), + }, + { + description: 'converts empty hash comments', + input: joinAsWindowsLines( + 'Write-Host "Comment without text" #', + 'Write-Host "Non-empty line"', + ), + expectedOutput: getInlinedOutputWithSemicolons( + 'Write-Host "Comment without text" <##>', + 'Write-Host "Non-empty line"', + ), + }, + { + description: 'adds whitespaces around comments', + input: joinAsWindowsLines( + '#Comment line with no whitespaces around', + 'Write-Host "Hello"#Comment in the end with no whitespaces around', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '<# Comment line with no whitespaces around #>', + 'Write-Host "Hello"<# Comment in the end with no whitespaces around #>', + ), + }, + { + description: 'trims whitespaces around comments', + input: joinAsWindowsLines( + '# Comment with whitespaces around ', + '#\tComment with tabs around\t\t', + '#\t Comment with tabs and whitespaces around \t \t', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '<# Comment with whitespaces around #>', + '<# Comment with tabs around #>', + '<# Comment with tabs and whitespaces around #>', + ), + }, + { + description: 'preserves block comments', + input: joinAsWindowsLines( + '$text = "Hello"\t<# block comment #> + "World"', + '$text = "Hello"\t+<#comment#>"World"', + '<# Block comment in a line #>', + 'Write-Host "Hello world <# Block comment in the end of line #>', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '$text = "Hello"\t<# block comment #> + "World"', + '$text = "Hello"\t+<#comment#>"World"', + '<# Block comment in a line #>', + 'Write-Host "Hello world <# Block comment in the end of line #>', + ), + }, + { + description: 'preserves single-line input', + input: 'Write-Host "expected" # as it is!', + expectedOutput: 'Write-Host "expected" # as it is!', + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateDoUntilTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateDoUntilTests.ts new file mode 100644 index 00000000..8b07b48a --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateDoUntilTests.ts @@ -0,0 +1,407 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createDoUntilTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_do?view=powershell-7.4 + return [ + { + description: 'do-until loop without newlines', + input: 'do { $i++; Write-Host $i } until ($i -ge 5)', + expectedOutput: 'do { $i++; Write-Host $i } until ($i -ge 5)', + }, + { + description: 'simple do-until loop (single line inside do block)', + input: joinAsWindowsLines( + 'do {', + ' $i++', + '} until ($i -lt 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -lt 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'multiline do-until loop', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' Write-Host $i', + '} until ($i -ge 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $i') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -ge 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'nested do-until loops', + input: joinAsWindowsLines( + 'do {', + ' $outer = 0', + ' do {', + ' $inner = 0', + ' do {', + ' $inner++', + ' Write-Host "Inner: $inner"', + ' } until ($inner -ge 3)', + ' $outer++', + ' Write-Host "Outer: $outer"', + ' } until ($outer -ge 2)', + ' $mainCounter++', + ' Write-Host "Main: $mainCounter"', + '} until ($mainCounter -ge 2)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$outer = 0') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$inner = 0') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$inner++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Inner: $inner"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($inner -ge 3)') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$outer++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Outer: $outer"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($outer -ge 2)') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$mainCounter++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Main: $mainCounter"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($mainCounter -ge 2)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-until loop with condition on separate line', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' Write-Host $i', + '}', + 'until ($i -ge 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $i') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') // No semicolon after this to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -ge 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-until loop with complex condition', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' $j--', + '} until ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks + ' $i -ge 10 -or `', + ' $j -le 0 `', + ')', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$j--') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i -ge 10 -or') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$j -le 0') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-until loop with nested if statement', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' if ($i % 2 -eq 0) {', + ' Write-Host "Even: $i"', + ' } else {', + ' Write-Host "Odd: $i"', + ' }', + '} until ($i -ge 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('if ($i % 2 -eq 0)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Even: $i"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('else') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Odd: $i"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -ge 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-until loop with semicolon after closing brace', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' Write-Host $i', + '};', + 'until ($i -ge 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $i') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -ge 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: '-until loop with pipeline in condition', + input: joinAsWindowsLines( + 'do {', + ' $result = Get-Something', + ' Process-Result $result', + '} until ($result | Test-Condition)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = Get-Something') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Process-Result $result') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result | Test-Condition') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-until loop with multiline condition', + input: joinAsWindowsLines( + 'do {', + ' $result = Get-Something', + ' Process-Result $result', + '} until ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks + ' $result -and (-Not $result) `', + ')', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = Get-Something') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Process-Result $result') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result -and (-Not $result)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-until loop with script block condition', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' Write-Host $i', + '} until ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks + ' & {', + ' param($val)', + ' $val -ge 5', + ' } $i `', + ')', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $i') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('until') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('&') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('param($val)') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$val -ge 5') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-until after closing bracket', + input: joinAsWindowsLines( + 'switch ($value) { default { Write-Host "Default" } }', + 'do { $i++ } until ($i -ge 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('switch ($value) { default { Write-Host "Default" } }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('do { $i++ } until ($i -ge 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateDoWhileTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateDoWhileTests.ts new file mode 100644 index 00000000..e4c13e47 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateDoWhileTests.ts @@ -0,0 +1,281 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createDoWhileTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_do?view=powershell-7.4 + return [ + { + description: 'do-while loop without newlines', + input: 'do { $i++ } while ($i -lt 5)', + expectedOutput: 'do { $i++ } while ($i -lt 5)', + }, + { + description: 'simple do-while loop (single line inside do block)', + input: joinAsWindowsLines( + 'do {', + ' $i++', + '} while ($i -lt 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -lt 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'multiline do-while loop', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' Write-Host $i', + '} while ($i -lt 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $i') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -lt 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'nested do-while loops', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' do {', + ' $j++', + ' } while ($j -lt 3)', + ' Write-Host "$i, $j"', + '} while ($i -lt 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$j++') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($j -lt 3)') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "$i, $j"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -lt 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-while loop with condition on separate line', + input: joinAsWindowsLines( + 'do {', + ' $i++', + '}', + 'while ($i -lt 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') // No semicolon after this to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -lt 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-while loop with multiline condition', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' $j--', + '} while ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks + ' $i -lt 10 -and `', + ' $j -gt 0 `', + ')', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$j--') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i -lt 10 -and') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$j -gt 0') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-while loop with nested if statement', + input: joinAsWindowsLines( + 'do {', + ' $i++', + ' if ($i % 2 -eq 0) {', + ' Write-Host "Even"', + ' } else {', + ' Write-Host "Odd"', + ' }', + '} while ($i -lt 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('if ($i % 2 -eq 0)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Even"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('else') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Odd"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -lt 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-while loop with semicolon after closing brace', + input: joinAsWindowsLines( + 'do {', + ' $i++', + '};', + 'while ($i -lt 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -lt 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-while loop with pipeline in condition', + input: joinAsWindowsLines( + 'do {', + ' $result = Get-Something', + '} while ( $result | Test-Condition )', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('do') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = Get-Something') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result | Test-Condition') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'do-while after closing bracket', + input: joinAsWindowsLines( + 'if ($someCondition) { $variable = "Some value" }', + 'do { $i++; Write-Host $i } while ($i -lt 5)', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if ($someCondition) { $variable = "Some value" }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('do { $i++; Write-Host $i } while ($i -lt 5)') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateForLoopTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateForLoopTests.ts new file mode 100644 index 00000000..01715719 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateForLoopTests.ts @@ -0,0 +1,214 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createForLoopTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_for?view=powershell-7.4 + // Known limitations: + // - Multiline for loop with sections without semicolon, e.g. `for(\n$i =0\n$i - l5\n$i++\n)` + return [ + { + description: 'for loop without newlines', + input: 'for ($i = 0; $i -lt 5; $i++) { Write-Host $i }', + expectedOutput: 'for ($i = 0; $i -lt 5; $i++) { Write-Host $i }', + }, + { + description: 'simple for loop (single line inside code block)', + input: joinAsWindowsLines( + 'for ($i = 0; $i -lt 5; $i++) {', + ' Write-Host $i', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('for') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i = 0; $i -lt 5; $i++)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $i') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'multiline for loop', + input: joinAsWindowsLines( + 'for ($i = 0; $i -lt 5; $i++) {', + ' Write-Host "Current value: $i"', + ' $result += $i', + ' Do-SomethingWith $i', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('for') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i = 0; $i -lt 5; $i++)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Current value: $i"') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result += $i') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Do-SomethingWith $i') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'nested for loops', + input: joinAsWindowsLines( + 'for ($i = 0; $i -lt 3; $i++) {', + ' for ($j = 0; $j -lt 2; $j++) {', + ' Write-Host "i: $i, j: $j"', + ' }', + ' Write-Host "Outer loop: $i"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('for') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i = 0; $i -lt 3; $i++)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('for') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($j = 0; $j -lt 2; $j++)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "i: $i, j: $j"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Outer loop: $i"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'for loop without spaces in header', + input: joinAsWindowsLines( + 'for($i=0;$i-lt5;$i++){', + ' Write-Host $i', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('for') + .withLiteralString('($i=0;$i-lt5;$i++)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $i') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'for loop with empty sections', + input: joinAsWindowsLines( + 'for (;;) {', + ' $i++', + ' if ($i -ge 5) { break }', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('for') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(;;)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('if ($i -ge 5) { break }') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'for loop with multiple statements in each section', + input: joinAsWindowsLines( + 'for ($i = 0, $j = 10; $i -lt 5 -and $j -gt 0; $i++, $j--) {', + ' Write-Host "i: $i, j: $j"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('for') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i = 0, $j = 10; $i -lt 5 -and $j -gt 0; $i++, $j--)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "i: $i, j: $j"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'for loop with multiline sections', + input: joinAsWindowsLines( + 'for ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks + ' $i = 0; `', + ' $i -lt 5; `', + ' $i++ `', + ') {', + ' Write-Host $i', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('for') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i = 0;') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i -lt 5;') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $i') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'for after closing bracket', + input: joinAsWindowsLines( + '$scriptBlock = { Write-Host "Inside script block" }', + 'for ($i = 0; $i -lt 5; $i++) { Write-Host $i }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('$scriptBlock = { Write-Host "Inside script block" }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('for ($i = 0; $i -lt 5; $i++) { Write-Host $i }') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateForeachTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateForeachTests.ts new file mode 100644 index 00000000..db9bd8dd --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateForeachTests.ts @@ -0,0 +1,284 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createForeachTests(): PipeTestScenario[] { + return [ + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_foreach?view=powershell-7.4 + { + description: 'foreach loop without newlines', + input: 'foreach ($item in $collection) { Write-Host $item }', + expectedOutput: 'foreach ($item in $collection) { Write-Host $item }', + }, + { + description: 'simple foreach loop (single line inside code block)', + input: joinAsWindowsLines( + 'foreach ($item in $collection) {', + ' Write-Host $item', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('foreach') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($item in $collection)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $item') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'multiline foreach loop', + input: joinAsWindowsLines( + 'foreach ($item in $collection) {', + ' $processedItem = $item.ToUpper()', + ' Write-Host "Processing: $processedItem"', + ' $result += $processedItem', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('foreach') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($item in $collection)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$processedItem = $item.ToUpper()') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Processing: $processedItem"') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result += $processedItem') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'nested foreach loops', + input: joinAsWindowsLines( + 'foreach ($outer in $outerCollection) {', + ' Write-Host "Outer: $outer"', + ' foreach ($inner in $innerCollection) {', + ' Write-Host " Inner: $inner"', + ' $result = "$outer-$inner"', + ' $combinedResults += $result', + ' }', + ' Write-Host "Completed inner loop for $outer"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('foreach') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($outer in $outerCollection)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Outer: $outer"') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('foreach') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($inner in $innerCollection)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host " Inner: $inner"') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = "$outer-$inner"') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$combinedResults += $result') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Completed inner loop for $outer"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'foreach loop with multiline condition', + input: joinAsWindowsLines( + 'foreach ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks + ' $item `', + ' in `', + ' $collection `', + ') {', + ' Write-Host $item', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('foreach') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$item') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('in') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$collection') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $item') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'foreach loop with pipeline in collection', + input: joinAsWindowsLines( + 'foreach ($item in Get-Process | Where-Object { $_.CPU -gt 50 }) {', + ' Write-Host $item.Name', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('foreach') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($item in Get-Process |') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Where-Object { $_.CPU -gt 50 })') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $item.Name') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'foreach loop with complex collection expression', + input: joinAsWindowsLines( + 'foreach ($item in ( `', + ' $array1 + `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks + ' $array2 | `', + ' Where-Object { $_ -ne $null } `', + ')) {', + ' Write-Host $item', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('foreach') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($item in (') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$array1 +') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$array2 |') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Where-Object { $_ -ne $null }') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('))') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $item') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'foreach loop with script block in collection', + input: joinAsWindowsLines( + 'foreach ($item in & {', + ' param($start, $end)', + ' $start..$end', + '} 1 10) {', + ' Write-Host $item', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('foreach') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($item in &') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('param($start, $end)') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$start..$end') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('1 10)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $item') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'foreach loop with closing brace on same line as last statement', + input: joinAsWindowsLines( + 'foreach ($item in $collection) {', + ' Write-Host $item', + ' Process-Item $item }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('foreach') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($item in $collection)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $item') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Process-Item $item') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'foreach after closing bracket', + input: joinAsWindowsLines( + 'function Test-Function { Write-Host "Test" }', + 'foreach ($item in @(1,2,3)) { Write-Host $item }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('function Test-Function { Write-Host "Test" }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('foreach ($item in @(1,2,3)) { Write-Host $item }') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateFunctionTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateFunctionTests.ts new file mode 100644 index 00000000..b166abfd --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateFunctionTests.ts @@ -0,0 +1,229 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createFunctionTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions?view=powershell-7.4 + // Known limitations: + // - Functions with advanced parameters are not yet supported. + return [ + { + description: 'function without newlines', + input: 'function Get-Name { param($FirstName, $LastName) Write-Output "$FirstName $LastName" }', + expectedOutput: 'function Get-Name { param($FirstName, $LastName) Write-Output "$FirstName $LastName" }', + }, + { + description: 'simple function (single line inside code block)', + input: joinAsWindowsLines( + 'function Say-Hello {', + ' Write-Host "Hello, World!"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('function') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Say-Hello') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Hello, World!"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'function with multiple statements', + input: joinAsWindowsLines( + 'function Do-Something {', + ' $result = Get-Something', + ' Process-Result $result', + ' Write-Output "Done processing"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('function') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Do-Something') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = Get-Something') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Process-Result $result') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Output "Done processing"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'function with begin, process, and end blocks', + input: joinAsWindowsLines( + 'function Process-Collection {', + ' begin {', + ' $total = 0', + ' }', + ' process {', + ' $total += $_', + ' }', + ' end {', + ' Write-Output "Total: $total"', + ' }', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('function') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Process-Collection') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('begin') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$total = 0') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('process') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$total += $_') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('end') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Output "Total: $total"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'nested functions', + input: joinAsWindowsLines( + 'function Outer-Function {', + ' param($outerParam)', + ' function Inner-Function {', + ' param($innerParam)', + ' Write-Output "Inner: $innerParam"', + ' }', + ' Write-Output "Outer: $outerParam"', + ' Inner-Function -innerParam "Hello from inner"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('function') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Outer-Function') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('param($outerParam)') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('function') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Inner-Function') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('param($innerParam)') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Output "Inner: $innerParam"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Output "Outer: $outerParam"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Inner-Function -innerParam "Hello from inner"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'function without space before opening brace', + input: joinAsWindowsLines( + 'function Get-Something{', + ' return "Something"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('function') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Get-Something') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('return "Something"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'function with single-line param block', + input: joinAsWindowsLines( + 'function Set-Value {', + ' param([string]$key, [object]$value)', + ' $hash[$key] = $value', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('function') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Set-Value') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('param([string]$key, [object]$value)') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$hash[$key] = $value') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'function after closing bracket', + input: joinAsWindowsLines( + 'if ($condition) { $value = 10 }', + 'function Test-Function { param($param) Write-Host $param }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if ($condition) { $value = 10 }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('function Test-Function { param($param) Write-Host $param }') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateHereStringTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateHereStringTests.ts new file mode 100644 index 00000000..89a4568f --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateHereStringTests.ts @@ -0,0 +1,240 @@ +import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; +import type { PipeTestScenario } from '../PipeTestRunner'; + +export function createHereStringTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings + return [ + { + description: 'double-quoted here-string', + input: joinAsWindowsLines( + '@"', + 'Lorem', + 'ipsum', + 'dolor sit amet', + '"@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + joinLinesForDoubleQuotedString( + '"Lorem', + 'ipsum', + 'dolor sit amet"', + ), + ), + }, + { + description: 'single-quoted here-string', + input: joinAsWindowsLines( + '@\'', + 'Lorem', + 'ipsum', + 'dolor sit amet', + '\'@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + joinLinesForSingleQuotedString( + 'Lorem', + 'ipsum', + 'dolor sit amet', + ), + ), + }, + { + description: 'preserves invalid here-string syntax', + input: joinAsWindowsLines( + '@" invalid syntax', + 'I will not be processed as here-string', + '"@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '@" invalid syntax', + 'I will not be processed as here-string', + '"@', + ), + }, + { + description: 'preserves here-string with character before terminator', + input: joinAsWindowsLines( + '@\'', + 'do not match here', + ' \'@', + 'character \'@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '@\'', + 'do not match here', + ' \'@', + 'character \'@', + ), + }, + { + description: 'preserves here-string with mismatched delimiters', + input: joinAsWindowsLines( + '@\'', + 'lorem', + '"@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '@\'', + 'lorem', + '"@', + ), + }, + { + description: 'single quoted here-string with nested single quoted here-string', + input: joinAsWindowsLines( + '$hasInnerDoubleQuotedTerminator = @"', + 'inner text', + '@\'', + 'inner terminator text', + '\'@', + '"@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + joinLinesForDoubleQuotedString( + '$hasInnerDoubleQuotedTerminator = "inner text', + '@\'', + 'inner terminator text', + '\'@"', + ), + ), + }, + { + description: 'single quoted here-string with inner double-quoted string', + input: joinAsWindowsLines( + '$hasInnerSingleQuotedTerminator = @\'', + 'inner text', + '@"', + 'inner terminator text', + '"@', + '\'@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + joinLinesForSingleQuotedString( + '$hasInnerSingleQuotedTerminator = \'inner text', + '@"', + 'inner terminator text', + '"@\'', + ), + ), + }, + { + description: 'here-string with character after terminator', + input: joinAsWindowsLines( + '@\'', + 'lorem', + '\'@ after', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '\'lorem\' after', + ), + }, + { + description: 'escapes double quotes in double-quoted here-string', + input: joinAsWindowsLines( + '@"', + 'For help, type "get-help"', + '"@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '"For help, type `"get-help`""', + ), + }, + { + description: 'escapes single quotes in single-quoted here-string', + input: joinAsWindowsLines( + '@\'', + 'For help, type \'get-help\'', + '\'@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '\'For help, type \'\'get-help\'\'\'', + ), + }, + { + description: 'here-string not at line start', + input: joinAsWindowsLines( + '$page = [XML] @"', + 'multi-lined', + 'and "quoted"', + '"@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + joinLinesForDoubleQuotedString( + '$page = [XML] "multi-lined', + 'and `"quoted`""', + ), + ), + }, + { + description: 'trims whitespace after here-string header', + input: joinAsWindowsLines( + '@" \t', + 'text with whitespaces at here-string start', + '"@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + '"text with whitespaces at here-string start"', + ), + }, + { + description: 'preserves whitespace in here-string lines', + input: joinAsWindowsLines( + '@\'', + '\ttext with tabs around\t\t', + ' text with whitespace around ', + '\'@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + joinLinesForSingleQuotedString( + '\'\ttext with tabs around\t\t', + ' text with whitespace around \'', + ), + ), + }, + { + description: 'preserves code inside here-string', + input: joinAsWindowsLines( // Triggering a some code inlining logic: + '@"', + 'if (', + ' $condition1 -and', + ' $condition2', + ') {', + ' Write-Host "True"', + ' Write-Warning "Not false"', + '} else', + '{', + ' Get-Process `', + ' | Where-Object { $_.CPU -gt 50 }', + '}', + '"@', + ), + expectedOutput: getInlinedOutputWithSemicolons( + joinLinesForDoubleQuotedString( // Identical to input + '"if (', + ' $condition1 -and', + ' $condition2', + ') {', + ' Write-Host `"True`"', + ' Write-Warning `"Not false`"', + '} else', + '{', + ' Get-Process `', + ' | Where-Object { $_.CPU -gt 50 }', + '}"', + ), + ), + }, + ]; +} + +function joinLinesForDoubleQuotedString( + ...lines: readonly string[] +): string { + return lines.join('`r`n'); +} + +function joinLinesForSingleQuotedString( + ...lines: readonly string[] +): string { + return lines.join('\'+"`r`n"+\''); +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateIfStatementTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateIfStatementTests.ts new file mode 100644 index 00000000..6b60fa47 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateIfStatementTests.ts @@ -0,0 +1,426 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createIfStatementTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_if?view=powershell-7.4 + return [ + { + description: 'if/else statement without newlines', + input: 'if ($condition) { Write-Host "True" } else { Write-Host "False" }', + expectedOutput: 'if ($condition) { Write-Host "True" } else { Write-Host "False" }', + }, + { + description: 'simple if statement (single line inside code block)', + input: joinAsWindowsLines( + 'if ($true) {', + ' Write-Host "True"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($true)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "True"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'if/else statement', + input: joinAsWindowsLines( + 'if ($condition) {', + ' Write-Host "True"', + '} else {', + ' Write-Host "False"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($condition)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "True"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('else') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "False"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'if/elseif/else statement', + input: joinAsWindowsLines( + 'if ($condition1) {', + ' Write-Host "Condition 1"', + '} elseif ($condition2) {', + ' Write-Host "Condition 2"', + '} else {', + ' Write-Host "None"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($condition1)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Condition 1"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('elseif') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($condition2)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Condition 2"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('else') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "None"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'if statement with multiple lines', + input: joinAsWindowsLines( + 'if ($condition) {', + ' $result = 10 * 5', + ' Write-Host "Calculation done"', + ' $finalResult = $result + 20', + ' Write-Host "Final result: $finalResult"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($condition)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 * 5') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Calculation done"') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$finalResult = $result + 20') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Final result: $finalResult"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'nested if statements', + input: joinAsWindowsLines( + 'if ($outerCondition) {', + ' $outerResult = 10', + ' if ($innerCondition1) {', + ' Write-Host "Inner condition 1 met"', + ' } elseif ($innerCondition2) {', + ' Write-Host "Inner condition 2 met"', + ' } else {', + ' Write-Host "No inner conditions met"', + ' }', + ' Write-Host "Outer condition processing complete"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($outerCondition)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$outerResult = 10') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($innerCondition1)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Inner condition 1 met"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('elseif') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($innerCondition2)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Inner condition 2 met"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('else') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "No inner conditions met"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Outer condition processing complete"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'if/elseif/else with closing brackets on separate lines', + input: joinAsWindowsLines( + 'if ($condition1) {', + ' Write-Host "Condition 1"', + '}', + 'elseif ($condition2) {', + ' Write-Host "Condition 2"', + '}', + 'else {', + ' Write-Host "No condition met"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($condition1)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Condition 1"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') // No semicolon after this to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('elseif') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($condition2)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Condition 2"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') // No semicolon after this to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('else') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "No condition met"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'if statement without space before opening parenthesis', + input: joinAsWindowsLines( + 'if($condition) {', + ' Write-Host "True"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if($condition)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "True"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'if statement without space after closing parenthesis', + input: joinAsWindowsLines( + 'if ($condition){', + ' Write-Host "True"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if ($condition)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "True"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'if statement with extra spaces in condition', + input: joinAsWindowsLines( + 'if ( $condition ) {', + ' Write-Host "True"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$condition') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "True"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'if statement with multiline condition', + input: joinAsWindowsLines( + 'if ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks + ' $condition1 -and `', + ' $condition2 `', + ') {', + ' Write-Host "True"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$condition1 -and') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$condition2') + .withOptionalWhitespaceButNoNewline() + .withLiteralString(')') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "True"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'if/elseif/else with mixed brace styles', + input: joinAsWindowsLines( + 'if ($condition1) {', + ' Write-Host "Condition 1"', + '} elseif ($condition2)', + '{', + ' Write-Host "Condition 2"', + '} else {', + ' Write-Host "None"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($condition1)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Condition 1"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('elseif') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($condition2)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Condition 2"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('else') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "None"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'if statement with pipeline in condition', + input: joinAsWindowsLines( + 'if (Get-Process | Where-Object { $_.CPU -gt 50 }) {', + ' Write-Host "High CPU usage detected"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('(Get-Process |') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Where-Object { $_.CPU -gt 50 })') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "High CPU usage detected"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'if statement after closing bracket', + input: joinAsWindowsLines( + '$testArray = @(1, 2, 3, 4, 5)', + '$result = $testArray | ForEach-Object { $_ * 2 }', + 'if ($result.Count -gt 0) { Write-Host "Array has items" }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('$testArray = @(1, 2, 3, 4, 5)') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = $testArray | ForEach-Object { $_ * 2 }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('if ($result.Count -gt 0) { Write-Host "Array has items" }') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateLineContinuationBacktickTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateLineContinuationBacktickTests.ts new file mode 100644 index 00000000..699a0a64 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateLineContinuationBacktickTests.ts @@ -0,0 +1,61 @@ +import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; +import type { PipeTestScenario } from '../PipeTestRunner'; + +export function createLineContinuationBacktickCases(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.4#line-continuation + return [ + { + description: 'inlines newlines with trailing backtick', + input: joinAsWindowsLines( + 'Get-Service * `', + '| Format-Table -AutoSize', + ), + expectedOutput: getInlinedOutputWithSemicolons( + 'Get-Service * | Format-Table -AutoSize', + ), + }, + { + description: 'inlines newlines with trailing backtick and different line endings', + input: 'Get-Service `\n' + + '* `\r' + + '| Sort-Object StartType `\r\n' + + '| Format-Table -AutoSize', + expectedOutput: getInlinedOutputWithSemicolons( + 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize', + ), + }, + { + description: 'trims whitespace when inlining with trailing backtick', + input: joinAsWindowsLines( + 'Get-Service * `', + '\t| Sort-Object StartType `', + ' | Format-Table -AutoSize', + ), + expectedOutput: getInlinedOutputWithSemicolons( + 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize', + ), + }, + { + description: 'preserves line without whitespace before backtick', + input: joinAsWindowsLines( + 'Get-Service *`', + '| Format-Table -AutoSize', + ), + expectedOutput: getInlinedOutputWithSemicolons( + 'Get-Service *`', + '| Format-Table -AutoSize', + ), + }, + { + description: 'preserves line with characters after backtick', + input: joinAsWindowsLines( + 'line start ` after', + 'should not be wrapped', + ), + expectedOutput: getInlinedOutputWithSemicolons( + 'line start ` after', + 'should not be wrapped', + ), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateNewlineTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateNewlineTests.ts new file mode 100644 index 00000000..7db6142d --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateNewlineTests.ts @@ -0,0 +1,103 @@ +import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; +import type { PipeTestScenario } from '../PipeTestRunner'; + +export function createNewlineTests(): PipeTestScenario[] { + return [ + { + description: 'does not add semicolon to single line input', + input: 'Write-Host "Single line input"', + expectedOutput: 'Write-Host "Single line input"', + }, + { + description: 'does not add semicolon to last line of multiline input', + input: joinAsWindowsLines( + 'Write-Host "First line"', + 'Write-Host "Second line"', + ), + expectedOutput: 'Write-Host "First line"; Write-Host "Second line"', + }, + { + description: 'preserves existing semicolons', + input: joinAsWindowsLines( + 'line-without-semicolon', + 'line-with-semicolon;', + 'ending-line', + ), + expectedOutput: getInlinedOutputWithSemicolons( + 'line-without-semicolon', + 'line-with-semicolon', + 'ending-line', + ), + }, + { + description: 'inlines code with \\n newlines', + input: + '$things = Get-ChildItem C:\\Windows\\' + + '\nforeach ($thing in $things) {' + + '\nWrite-Host $thing.Name -ForegroundColor Magenta' + + '\n}', + expectedOutput: getInlinedOutputWithSemicolons( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), + }, + { + description: 'removes empty lines', + input: + '$things = Get-ChildItem C:\\Windows\\' + + '\n\nforeach ($thing in $things) {' + + '\n\nWrite-Host $thing.Name -ForegroundColor Magenta' + + '\n\n\n}', + expectedOutput: getInlinedOutputWithSemicolons( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), + }, + { + description: 'inlines code with \\r newlines', + input: + '$things = Get-ChildItem C:\\Windows\\' + + '\rforeach ($thing in $things) {' + + '\rWrite-Host $thing.Name -ForegroundColor Magenta' + + '\r}', + expectedOutput: getInlinedOutputWithSemicolons( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), + }, + { + description: 'inlines code with mixed newline types', + input: + '$things = Get-ChildItem C:\\Windows\\' + + '\r\nforeach ($thing in $things) {' + + '\n\rWrite-Host $thing.Name -ForegroundColor Magenta' + + '\n\r}', + expectedOutput: getInlinedOutputWithSemicolons( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), + }, + { + description: 'trims whitespace from lines', + input: + ' $things = Get-ChildItem C:\\Windows\\ ' + + '\nforeach ($thing in $things) {' + + '\n\tWrite-Host $thing.Name -ForegroundColor Magenta' + + '\r \n}', + expectedOutput: getInlinedOutputWithSemicolons( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateScriptBlockTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateScriptBlockTests.ts new file mode 100644 index 00000000..d8be212c --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateScriptBlockTests.ts @@ -0,0 +1,255 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createScriptBlockTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_script_blocks%3Fview=powershell-7.4 + return [ + { + description: 'simple script block without newlines', + input: '{ Write-Host "Hello, World!" }', + expectedOutput: '{ Write-Host "Hello, World!" }', + }, + { + description: 'multiline script block', + input: joinAsWindowsLines( + '{', + ' $result = 10 * 5', + ' Write-Host "The result is: $result"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 * 5') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "The result is: $result"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'script block with parameters', + input: joinAsWindowsLines( + '{', + ' param($p1, $p2)', + ' Write-Host "p1: $p1, p2: $p2"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('param($p1, $p2)') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "p1: $p1, p2: $p2"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'script block with typed parameters', + input: joinAsWindowsLines( + '{', + ' param([int]$p1, [string]$p2)', + ' Write-Host "p1: $p1, p2: $p2"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('param([int]$p1, [string]$p2)') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "p1: $p1, p2: $p2"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'script block with begin, process, and end blocks', + input: joinAsWindowsLines( + '{', + ' begin { $total = 0 }', + ' process { $total += $_ }', + ' end { Write-Host "Total: $total" }', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('begin') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$total = 0') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('process') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$total += $_') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('end') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Total: $total"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'nested script blocks', + input: joinAsWindowsLines( + '{', + ' $innerBlock = {', + ' param($x)', + ' Write-Host "Inner: $x"', + ' }', + ' & $innerBlock -x "Hello"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$innerBlock =') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('param($x)') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Inner: $x"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('& $innerBlock -x "Hello"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'script block with return statement', + input: joinAsWindowsLines( + '{', + ' $result = 10 * 5', + ' return $result', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 * 5') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('return $result') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'script block with pipeline input', + input: joinAsWindowsLines( + '{', + ' process {', + ' $_ | Where-Object { $_ % 2 -eq 0 }', + ' }', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('process') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$_ | Where-Object { $_ % 2 -eq 0 }') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'script block with dynamicparam block', + input: joinAsWindowsLines( + '{', + ' dynamicparam {', + ' $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary', + ' return $paramDictionary', + ' }', + ' process {', + ' Write-Host "Processing"', + ' }', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('dynamicparam') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('return $paramDictionary') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('process') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Processing"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'script block after closing bracket', + input: joinAsWindowsLines( + 'if ($condition) { $value = 10 }', + '$scriptBlock = { param($x) Write-Host $x }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if ($condition) { $value = 10 }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$scriptBlock = { param($x) Write-Host $x }') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateSwitchTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateSwitchTests.ts new file mode 100644 index 00000000..9357006d --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateSwitchTests.ts @@ -0,0 +1,330 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createSwitchTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_switch?view=powershell-7.4 + return [ + { + description: 'switch statement with no newlines', + input: 'switch ($value) { 1 { Write-Host "One" }; 2 { Write-Host "Two" }; default { Write-Host "Other" } }', + expectedOutput: 'switch ($value) { 1 { Write-Host "One" }; 2 { Write-Host "Two" }; default { Write-Host "Other" } }', + }, + { + description: 'simple switch statement (single line inside code block)', + input: joinAsWindowsLines( + 'switch ($value) {', + ' 1 { Write-Host "One" }', + ' 2 { Write-Host "Two" }', + ' default { Write-Host "Other" }', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('switch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($value)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('1') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "One"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('2') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Two"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('default') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Other"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'multiline switch statement', + input: joinAsWindowsLines( + 'switch ($value) {', + ' 1 {', + ' Write-Host "One"', + ' $result = 1', + ' }', + ' 2 {', + ' Write-Host "Two"', + ' $result = 2', + ' }', + ' default {', + ' Write-Host "Other"', + ' $result = 0', + ' }', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('switch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($value)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('1') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "One"') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 1') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('2') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Two"') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 2') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('default') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Other"') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 0') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'nested switch statements', + input: joinAsWindowsLines( + 'switch ($outer) {', + ' 1 {', + ' switch ($inner) {', + ' "A" { Write-Host "1A" }', + ' "B" { Write-Host "1B" }', + ' }', + ' }', + ' 2 {', + ' switch ($inner) {', + ' "A" { Write-Host "2A" }', + ' "B" { Write-Host "2B" }', + ' }', + ' }', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('switch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($outer)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('1') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('switch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($inner)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('"A"') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "1A"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('"B"') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "1B"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('2') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('switch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($inner)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('"A"') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "2A"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('"B"') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "2B"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'switch statement with no spaces', + input: joinAsWindowsLines( + 'switch($value){1{Write-Host"One"}2{Write-Host"Two"}default{Write-Host"Other"}}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('switch') + .withLiteralString('($value)') + .withLiteralString('{') + .withLiteralString('1') + .withLiteralString('{') + .withLiteralString('Write-Host"One"') + .withOptionalSemicolon() + .withLiteralString('}') + .withLiteralString('2') + .withLiteralString('{') + .withLiteralString('Write-Host"Two"') + .withOptionalSemicolon() + .withLiteralString('}') + .withLiteralString('default') + .withLiteralString('{') + .withLiteralString('Write-Host"Other"') + .withOptionalSemicolon() + .withLiteralString('}') + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'switch statement with regex matches', + input: joinAsWindowsLines( + 'switch -Regex ($value) {', + ' "^A.*" { Write-Host "Starts with A" }', + ' ".*Z$" { Write-Host "Ends with Z" }', + ' Default { Write-Host "No match" }', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('switch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('-Regex') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($value)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('"^A.*"') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Starts with A"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('".*Z$"') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Ends with Z"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Default') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "No match"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .buildRegex(), + }, + { + description: 'switch after closing bracket', + input: joinAsWindowsLines( + 'if ($condition) { $value = 10 }', + 'switch ($value) { 1 { "One" } 2 { "Two" } default { "Other" } }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if ($condition) { $value = 10 }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('switch ($value) { 1 { "One" } 2 { "Two" } default { "Other" } }') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateTryCatchFinallyTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateTryCatchFinallyTests.ts new file mode 100644 index 00000000..497c2079 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateTryCatchFinallyTests.ts @@ -0,0 +1,451 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createTryCatchFinallyTests(): PipeTestScenario[] { + // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_try_catch_finally?view=powershell-7.4 + return [ + { + description: 'try/catch/finally block without newlines', + input: 'try { $result = 10 / 0 } catch { Write-Host "An error occurred" } finally { Write-Host "Cleanup" }', + expectedOutput: 'try { $result = 10 / 0 } catch { Write-Host "An error occurred" } finally { Write-Host "Cleanup" }', + }, + { + description: 'simple try/catch block (single line inside code blocks)', + input: joinAsWindowsLines( + 'try {', + ' $result = 10 / 0', + '} catch {', + ' Write-Warning "An error occurred"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 / 0') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch {') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Warning "An error occurred"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'multiline try/catch block', + input: joinAsWindowsLines( + 'try {', + ' $result = 10 / 0', + ' Write-Host "Succesfully completed"', + '} catch {', + ' Write-Warning "An error occurred"', + ' Write-Host "Wrong division?"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 / 0') + .withSemicolon() // Ensure it adds semicolon to multiline text + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Succesfully completed"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch {') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Warning "An error occurred"') + .withSemicolon() // Ensure it adds semicolon to multiline text + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Wrong division?"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'try/finally block', + input: joinAsWindowsLines( + 'try {', + ' $result = 10 / 0', + '} finally {', + ' Write-Warning "An error occurred"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 / 0') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('finally {') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Warning "An error occurred"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'try/catch/finally block', + input: joinAsWindowsLines( + 'try {', + ' $result = 10 / 0', + '} catch {', + ' Write-Host "An error occurred"', + '} finally {', + ' Write-Host "Cleanup"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 / 0') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch {') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "An error occurred"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('finally {') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Cleanup"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'try/catch/finally with closing brackets on separate lines', + input: joinAsWindowsLines( + 'try {', + ' $result = 10 / 0', + '}', + 'catch {', + ' Write-Host "An error occurred"', + '}', + 'finally {', + ' Write-Host "Cleanup"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 / 0') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() // No semicolon after this to prevent runtime errors + .withLiteralString('catch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "An error occurred"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() // No semicolon after this to prevent runtime errors + .withLiteralString('finally') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Cleanup"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'try/catch with empty catch block', + input: joinAsWindowsLines( + 'try {', + ' $result = 10 / 0', + '} catch {}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 / 0') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch {}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: '/catch/finally with empty blocks', + input: joinAsWindowsLines( + 'try {} catch {} finally {}', + ), + expectedOutput: getInlinedOutputWithSemicolons( + 'try {} catch {} finally {}', + ), + }, + { + description: 'try/catch with specific exception type', + input: joinAsWindowsLines( + 'try {', + ' throw [System.DivideByZeroException]::new()', + '} catch [System.DivideByZeroException] {', + ' Write-Host "Caught divide by zero exception"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('throw [System.DivideByZeroException]::new()') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch [System.DivideByZeroException]') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Caught divide by zero exception"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'try/catch with multiple specific exception types', + input: joinAsWindowsLines( + 'try {', + ' throw [System.IO.FileNotFoundException]::new()', + '} catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException] {', + ' Write-Host "Caught file or directory not found exception"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('throw [System.IO.FileNotFoundException]::new()') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException]') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Caught file or directory not found exception"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'try/catch with multiple catch blocks', + input: joinAsWindowsLines( + 'try {', + ' $result = 10 / 0', + '} catch [System.DivideByZeroException] {', + ' Write-Host "Caught divide by zero exception"', + '} catch [System.Exception] {', + ' Write-Host "Caught general exception"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$result = 10 / 0') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch [System.DivideByZeroException]') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Caught divide by zero exception"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch [System.Exception]') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Caught general exception"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'try/catch with exception variable', + input: joinAsWindowsLines( + 'try {', + ' throw "Custom error"', + '} catch {', + ' Write-Host "Error: $($_.Exception.Message)"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('throw "Custom error"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Error: $($_.Exception.Message)"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'try/catch/finally with return in try block', + input: joinAsWindowsLines( + 'try {', + ' return "Success"', + '} catch {', + ' Write-Host "An error occurred"', + '} finally {', + ' Write-Host "Cleanup"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('return "Success"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "An error occurred"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('finally') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Cleanup"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'nested try/catch blocks', + input: joinAsWindowsLines( + 'try {', + ' try {', + ' throw "Inner exception"', + ' } catch {', + ' throw "Outer exception"', + ' }', + '} catch {', + ' Write-Host "Caught in outer catch: $($_.Exception.Message)"', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('try') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('throw "Inner exception"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('throw "Outer exception"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('catch') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Caught in outer catch: $($_.Exception.Message)"') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'try after closing bracket', + input: joinAsWindowsLines( + 'if ($condition) { $value = 10 }', + 'try { $result = 10 / $value }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('if ($condition) { $value = 10 }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('try { $result = 10 / $value }') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateWhileTests.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateWhileTests.ts new file mode 100644 index 00000000..c0010175 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShellTests/CreateWhileTests.ts @@ -0,0 +1,222 @@ +import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner'; +import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities'; + +export function createWhileTests(): PipeTestScenario[] { + return [ + { + description: 'while loop without newlines', + input: 'while ($val -ne 3) { $val++ Write-Host $val }', + expectedOutput: 'while ($val -ne 3) { $val++ Write-Host $val }', + }, + { + description: 'simple while loop (single line inside code block)', + input: joinAsWindowsLines( + 'while ($i -lt 5) {', + ' $i++', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($i -lt 5)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$i++') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'multiline while loop', + input: joinAsWindowsLines( + 'while ($condition) {', + ' Do-Something', + ' Write-Host "Processing..."', + ' $condition = Test-Condition', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($condition)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Do-Something') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host "Processing..."') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$condition = Test-Condition') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'nested while loops', + input: joinAsWindowsLines( + 'while ($outerCondition) {', + ' $innerCounter = 0', + ' while ($innerCounter -lt 3) {', + ' Do-InnerTask', + ' $innerCounter++', + ' }', + ' Do-OuterTask', + ' $outerCondition = Test-OuterCondition', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($outerCondition)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$innerCounter = 0') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($innerCounter -lt 3)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Do-InnerTask') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$innerCounter++') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Do-OuterTask') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$outerCondition = Test-OuterCondition') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'while loop without space before opening parenthesis', + input: joinAsWindowsLines( + 'while($val -ne 3) {', + ' $val++', + ' Write-Host $val', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('while') + .withLiteralString('($val -ne 3)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$val++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $val') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'while loop without space after closing parenthesis', + input: joinAsWindowsLines( + 'while ($val -ne 3){', + ' $val++', + ' Write-Host $val', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($val -ne 3)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$val++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $val') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'while loop and trims extra spaces before statements', + input: joinAsWindowsLines( + 'while ($val -ne 3) {', + ' $val++', + ' Write-Host $val', + '}', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($val -ne 3)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$val++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $val') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'while loop with closing brace on same line as last statement', + input: joinAsWindowsLines( + 'while ($val -ne 3) {', + ' $val++', + ' Write-Host $val }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('while') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('($val -ne 3)') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('{') + .withOptionalWhitespaceButNoNewline() + .withLiteralString('$val++') + .withSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('Write-Host $val') + .withOptionalSemicolon() + .withOptionalWhitespaceButNoNewline() + .withLiteralString('}') + .withOptionalSemicolon() + .buildRegex(), + }, + { + description: 'while after closing bracket', + input: joinAsWindowsLines( + 'try { throw "Error" } catch { Write-Host "Caught error" }', + 'while ($true) { break }', + ), + expectedOutput: new RegexBuilder() + .withLiteralString('try { throw "Error" } catch { Write-Host "Caught error" }') + .withSemicolon() // Semicolon here to prevent runtime errors + .withOptionalWhitespaceButNoNewline() + .withLiteralString('while ($true) { break }') + .withOptionalSemicolon() + .buildRegex(), + }, + ]; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/PipeTestRunner.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/PipeTestRunner.ts new file mode 100644 index 00000000..ff539054 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/PipeTestRunner.ts @@ -0,0 +1,71 @@ +import { it, expect } from 'vitest'; +import type { Pipe } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/Pipe'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { indentText } from '@/application/Common/Text/IndentText'; + +export interface PipeTestScenario { + readonly description: string; + readonly input: string; + readonly expectedOutput: RegExp | string; +} + +export function runPipeTests( + pipe: Pipe, + testScenarios: readonly PipeTestScenario[], +) { + testScenarios.forEach(( + { input, description, expectedOutput: expectedInlinedOutput }, + ) => { + it(description, () => { + // act + const actualOutput = pipe.apply(input); + // assert + if (typeof expectedInlinedOutput === 'string') { + expect(actualOutput).to.equal(expectedInlinedOutput); + } else { + expect(actualOutput).to.match(expectedInlinedOutput, formatAssertionMessage([ + 'Regex did not match the output.', + 'Expected regex pattern:', + indentText(expectedInlinedOutput.toString()), + 'Actual output:', + indentText(actualOutput), + 'Given input:', + indentText(input), + ])); + } + }); + }); +} + +export class RegexBuilder { + private rawRegex: string = ''; + + public withLiteralString(string: string): this { + this.rawRegex += string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); + return this; + } + + public withSomeWhitespaceButNoNewLine(): this { + this.rawRegex += '[ \\t\\f]+'; + return this; + } + + public withOptionalWhitespaceButNoNewline(): this { + this.rawRegex += '[ \\t\\f]*'; + return this; + } + + public withOptionalSemicolon(): this { + this.rawRegex += ';?'; + return this; + } + + public withSemicolon(): this { + this.rawRegex += ';'; + return this; + } + + public buildRegex(): RegExp { + return new RegExp(this.rawRegex, 'g'); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeFactory.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeFactory.spec.ts new file mode 100644 index 00000000..f5222ecd --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeFactory.spec.ts @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'vitest'; +import { PipeFactory } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeFactory'; +import { PipeStub } from '@tests/unit/shared/Stubs/PipeStub'; +import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('PipeFactory', () => { + describe('ctor', () => { + it('throws when instances with same name is registered', () => { + // arrange + const duplicateName = 'duplicateName'; + const expectedError = `Pipe name must be unique: "${duplicateName}"`; + const pipes = [ + new PipeStub().withName(duplicateName), + new PipeStub().withName('uniqueName'), + new PipeStub().withName(duplicateName), + ]; + // act + const act = () => new PipeFactory(pipes); + // expect + expect(act).to.throw(expectedError); + }); + describe('throws when name is invalid', () => { + // act + const act = (invalidName: string) => new PipeFactory([new PipeStub().withName(invalidName)]); + // assert + testPipeNameValidation(act); + }); + }); + describe('get', () => { + describe('throws when name is invalid', () => { + // arrange + const sut = new PipeFactory(); + // act + const act = (invalidName: string) => sut.get(invalidName); + // assert + testPipeNameValidation(act); + }); + it('gets registered instance when it exists', () => { + // arrange + const expected = new PipeStub().withName('expectedName'); + const pipes = [expected, new PipeStub().withName('instanceToConfuse')]; + const sut = new PipeFactory(pipes); + // act + const actual = sut.get(expected.name); + // expect + expect(actual).to.equal(expected); + }); + it('throws when instance does not exist', () => { + // arrange + const missingName = 'missingName'; + const expectedError = `Unknown pipe: "${missingName}"`; + const pipes = []; + const sut = new PipeFactory(pipes); + // act + const act = () => sut.get(missingName); + // expect + expect(act).to.throw(expectedError); + }); + }); +}); + +function testPipeNameValidation(testRunner: (invalidName: string) => void) { + const testCases = [ + // Validate missing value + ...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true }) + .map((testCase) => ({ + name: `empty pipe name (${testCase.valueName})`, + value: testCase.absentValue, + expectedError: 'empty pipe name', + })), + // Validate camelCase + ...[ + 'PascalCase', + 'snake-case', + 'includesNumb3rs', + 'includes Whitespace', + 'noSpec\'ial', + ].map((nonCamelCaseValue) => ({ + name: `non camel case value (${nonCamelCaseValue})`, + value: nonCamelCaseValue, + expectedError: `Pipe name should be camelCase: "${nonCamelCaseValue}"`, + })), + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + // arrange + const invalidName = testCase.value; + const { expectedError } = testCase; + // act + const act = () => testRunner(invalidName); + // expect + expect(act).to.throw(expectedError); + }); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipelineCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipelineCompiler.spec.ts new file mode 100644 index 00000000..381318b6 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipelineCompiler.spec.ts @@ -0,0 +1,132 @@ +import { describe, it, expect } from 'vitest'; +import { PipelineCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipelineCompiler'; +import { type IPipelineCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/IPipelineCompiler'; +import type { IPipeFactory } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeFactory'; +import { PipeStub } from '@tests/unit/shared/Stubs/PipeStub'; +import { PipeFactoryStub } from '@tests/unit/shared/Stubs/PipeFactoryStub'; +import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('PipelineCompiler', () => { + describe('compile', () => { + describe('throws for invalid arguments', () => { + interface ITestCase { + readonly name: string; + readonly act: (test: PipelineTestRunner) => PipelineTestRunner; + readonly expectedError: string; + } + const testCases: ITestCase[] = [ + ...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true }) + .map((testCase) => ({ + name: `"value" is ${testCase.valueName}`, + act: (test) => test.withValue(testCase.absentValue), + expectedError: 'missing value', + })), + ...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true }) + .map((testCase) => ({ + name: `"pipeline" is ${testCase.valueName}`, + act: (test) => test.withPipeline(testCase.absentValue), + expectedError: 'missing pipeline', + })), + { + name: '"pipeline" does not start with pipe', + act: (test) => test.withPipeline('pipeline |'), + expectedError: 'pipeline does not start with pipe', + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + // act + const runner = new PipelineTestRunner(); + testCase.act(runner); + const act = () => runner.compile(); + // assert + expect(act).to.throw(testCase.expectedError); + }); + } + }); + describe('compiles pipeline as expected', () => { + const testCases = [ + { + name: 'compiles single pipe as expected', + pipes: [ + new PipeStub().withName('doublePrint').withApplier((value) => `${value}-${value}`), + ], + pipeline: '| doublePrint', + value: 'value', + expected: 'value-value', + }, + { + name: 'compiles multiple pipes as expected', + pipes: [ + new PipeStub().withName('prependLetterA').withApplier((value) => `A-${value}`), + new PipeStub().withName('prependLetterB').withApplier((value) => `B-${value}`), + ], + pipeline: '| prependLetterA | prependLetterB', + value: 'value', + expected: 'B-A-value', + }, + { + name: 'compiles with relaxed whitespace placing', + pipes: [ + new PipeStub().withName('appendNumberOne').withApplier((value) => `${value}1`), + new PipeStub().withName('appendNumberTwo').withApplier((value) => `${value}2`), + new PipeStub().withName('appendNumberThree').withApplier((value) => `${value}3`), + ], + pipeline: ' | appendNumberOne|appendNumberTwo| appendNumberThree', + value: 'value', + expected: 'value123', + }, + { + name: 'can reuse same pipe', + pipes: [ + new PipeStub().withName('removeFirstChar').withApplier((value) => `${value.slice(1)}`), + ], + pipeline: ' | removeFirstChar | removeFirstChar | removeFirstChar', + value: 'value', + expected: 'ue', + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + // arrange + const runner = new PipelineTestRunner() + .withValue(testCase.value) + .withPipeline(testCase.pipeline) + .withFactory(new PipeFactoryStub().withPipes(testCase.pipes)); + // act + const actual = runner.compile(); + // expect + expect(actual).to.equal(testCase.expected); + }); + } + }); + }); +}); + +class PipelineTestRunner implements IPipelineCompiler { + private value = 'non-empty-value'; + + private pipeline = '| validPipeline'; + + private factory: IPipeFactory = new PipeFactoryStub(); + + public withValue(value: string) { + this.value = value; + return this; + } + + public withPipeline(pipeline: string) { + this.pipeline = pipeline; + return this; + } + + public withFactory(factory: IPipeFactory) { + this.factory = factory; + return this; + } + + public compile(): string { + const sut = new PipelineCompiler(this.factory); + return sut.compile(this.value, this.pipeline); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser.spec.ts new file mode 100644 index 00000000..c1355125 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser.spec.ts @@ -0,0 +1,67 @@ +import { describe } from 'vitest'; +import { ParameterSubstitutionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser'; +import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition'; +import { SyntaxParserTestsRunner } from './SyntaxParserTestsRunner'; + +describe('ParameterSubstitutionParser', () => { + const sut = new ParameterSubstitutionParser(); + const runner = new SyntaxParserTestsRunner(sut); + describe('finds as expected', () => { + runner.expectPosition( + { + name: 'single parameter', + code: '{{ $parameter }}!', + expected: [new ExpressionPosition(0, 16)], + }, + { + name: 'different parameters', + code: 'He{{ $firstParameter }} {{ $secondParameter }}!!', + expected: [new ExpressionPosition(2, 23), new ExpressionPosition(24, 46)], + }, + { + name: 'tolerates lack of spaces around brackets', + code: 'He{{$firstParameter}}!!', + expected: [new ExpressionPosition(2, 21)], + }, + { + name: 'does not tolerate space after dollar sign', + code: 'He{{ $ firstParameter }}!!', + expected: [], + }, + ); + }); + describe('evaluates as expected', () => { + runner.expectResults( + { + name: 'single parameter', + code: '{{ $parameter }}', + args: (args) => args + .withArgument('parameter', 'Hello world'), + expected: ['Hello world'], + }, + { + name: 'different parameters', + code: '{{ $firstParameter }} {{ $secondParameter }}!', + args: (args) => args + .withArgument('firstParameter', 'Hello') + .withArgument('secondParameter', 'World'), + expected: ['Hello', 'World'], + }, + { + name: 'same parameters used twice', + code: '{{ $letterH }}e{{ $letterL }}{{ $letterL }}o Wor{{ $letterL }}d!', + args: (args) => args + .withArgument('letterL', 'l') + .withArgument('letterH', 'H'), + expected: ['H', 'l', 'l', 'l'], + }, + ); + }); + describe('compiles pipes as expected', () => { + runner.expectPipeHits({ + codeBuilder: (pipeline) => `{{ $argument${pipeline}}}`, + parameterName: 'argument', + parameterValue: 'value', + }); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/SyntaxParserTestsRunner.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/SyntaxParserTestsRunner.ts new file mode 100644 index 00000000..0eba4c84 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/SyntaxParserTestsRunner.ts @@ -0,0 +1,163 @@ +import { it, expect } from 'vitest'; +import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition'; +import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; +import { ExpressionEvaluationContextStub } from '@tests/unit/shared/Stubs/ExpressionEvaluationContextStub'; +import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub'; +import { scrambledEqual } from '@/application/Common/Array'; + +export class SyntaxParserTestsRunner { + constructor(private readonly sut: IExpressionParser) { + } + + public expectPosition(...testCases: ExpectPositionTestScenario[]) { + for (const testCase of testCases) { + it(testCase.name, () => { + // act + const expressions = this.sut.findExpressions(testCase.code); + // assert + const actual = expressions.map((e) => e.position); + expect(scrambledEqual(actual, testCase.expected)); + }); + } + return this; + } + + public expectNoMatch(...testCases: NoMatchTestScenario[]) { + this.expectPosition(...testCases.map((testCase) => ({ + name: testCase.name, + code: testCase.code, + expected: [], + }))); + } + + public expectResults(...testCases: ExpectResultTestScenario[]) { + for (const testCase of testCases) { + it(testCase.name, () => { + // arrange + const args = testCase.args(new FunctionCallArgumentCollectionStub()); + const context = new ExpressionEvaluationContextStub() + .withArgs(args); + // act + const expressions = this.sut.findExpressions(testCase.code); + // assert + const actual = expressions.map((e) => e.evaluate(context)); + expect(actual).to.deep.equal(testCase.expected); + }); + } + return this; + } + + public expectThrows(...testCases: ExpectThrowsTestScenario[]) { + for (const testCase of testCases) { + it(testCase.name, () => { + // arrange + const { expectedError } = testCase; + // act + const act = () => this.sut.findExpressions(testCase.code); + // assert + expect(act).to.throw(expectedError); + }); + } + return this; + } + + public expectPipeHits(data: ExpectPipeHitTestScenario) { + for (const validPipePart of PipeTestCases.ValidValues) { + this.expectHitPipePart(validPipePart, data); + } + for (const invalidPipePart of PipeTestCases.InvalidValues) { + this.expectMissPipePart(invalidPipePart, data); + } + } + + private expectHitPipePart(pipeline: string, data: ExpectPipeHitTestScenario) { + it(`"${pipeline}" hits`, () => { + // arrange + const expectedPipePart = pipeline.trim(); + const code = data.codeBuilder(pipeline); + const args = new FunctionCallArgumentCollectionStub() + .withArgument(data.parameterName, data.parameterValue); + const pipelineCompiler = new PipelineCompilerStub(); + const context = new ExpressionEvaluationContextStub() + .withPipelineCompiler(pipelineCompiler) + .withArgs(args); + // act + const expressions = this.sut.findExpressions(code); + expressions[0].evaluate(context); + // assert + expect(expressions).has.lengthOf(1); + expect(pipelineCompiler.compileHistory).has.lengthOf(1); + const actualPipePart = pipelineCompiler.compileHistory[0].pipeline; + const actualValue = pipelineCompiler.compileHistory[0].value; + expect(actualPipePart).to.equal(expectedPipePart); + expect(actualValue).to.equal(data.parameterValue); + }); + } + + private expectMissPipePart(pipeline: string, data: ExpectPipeHitTestScenario) { + it(`"${pipeline}" misses`, () => { + // arrange + const args = new FunctionCallArgumentCollectionStub() + .withArgument(data.parameterName, data.parameterValue); + const pipelineCompiler = new PipelineCompilerStub(); + const context = new ExpressionEvaluationContextStub() + .withPipelineCompiler(pipelineCompiler) + .withArgs(args); + const code = data.codeBuilder(pipeline); + // act + const expressions = this.sut.findExpressions(code); + expressions[0]?.evaluate(context); // Because an expression may include another with pipes + // assert + expect(pipelineCompiler.compileHistory).has.lengthOf(0); + }); + } +} + +interface ExpectResultTestScenario { + readonly name: string; + readonly code: string; + readonly args: ( + builder: FunctionCallArgumentCollectionStub, + ) => FunctionCallArgumentCollectionStub; + readonly expected: readonly string[]; +} + +interface ExpectThrowsTestScenario { + readonly name: string; + readonly code: string; + readonly expectedError: string; +} + +interface ExpectPositionTestScenario { + readonly name: string; + readonly code: string; + readonly expected: readonly ExpressionPosition[]; +} + +interface NoMatchTestScenario { + readonly name: string; + readonly code: string; +} + +interface ExpectPipeHitTestScenario { + readonly codeBuilder: (pipeline: string) => string; + readonly parameterName: string; + readonly parameterValue: string; +} + +const PipeTestCases = { + ValidValues: [ + // Single pipe with different whitespace combinations + ' | pipe', ' |pipe', '|pipe', ' |pipe', ' | pipe', + + // Double pipes with different whitespace combinations + ' | pipeFirst | pipeSecond', '| pipeFirst|pipeSecond', '|pipeFirst|pipeSecond', ' |pipeFirst |pipeSecond', '| pipeFirst | pipeSecond| pipeThird |pipeFourth', + ], + InvalidValues: [ + ' withoutPipeBefore |pipe', ' withoutPipeBefore', + + // It's OK to match them (move to valid values if needed) to let compiler throw instead. + '| pip€', '| pip{e} ', '| pipeWithNumber55', '| pipe with whitespace', + ], +}; diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser.spec.ts new file mode 100644 index 00000000..99fa809a --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser.spec.ts @@ -0,0 +1,272 @@ +import { describe } from 'vitest'; +import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition'; +import { WithParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser'; +import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; +import { SyntaxParserTestsRunner } from './SyntaxParserTestsRunner'; + +describe('WithParser', () => { + const sut = new WithParser(); + const runner = new SyntaxParserTestsRunner(sut); + describe('correctly identifies `with` syntax', () => { + runner.expectPosition( + { + name: 'when no context variable is not used', + code: 'hello {{ with $parameter }}no usage{{ end }} here', + expected: [new ExpressionPosition(6, 44)], + }, + { + name: 'when context variable is used', + code: 'used here ({{ with $parameter }}value: {{.}}{{ end }})', + expected: [new ExpressionPosition(11, 53)], + }, + { + name: 'when used twice', + code: 'first: {{ with $parameter }}value: {{ . }}{{ end }}, second: {{ with $parameter }}no usage{{ end }}', + expected: [new ExpressionPosition(7, 51), new ExpressionPosition(61, 99)], + }, + { + name: 'when nested', + code: 'outer: {{ with $outer }}outer value with context variable: {{ . }}, inner: {{ with $inner }}inner value{{ end }}.{{ end }}', + expected: [ + /* outer: */ new ExpressionPosition(7, 122), + /* inner: */ new ExpressionPosition(77, 112), + ], + }, + { + name: 'whitespaces: tolerate lack of whitespaces', + code: 'no whitespaces {{with $parameter}}value: {{ . }}{{end}}', + expected: [new ExpressionPosition(15, 55)], + }, + { + name: 'newlines: match multiline text', + code: 'non related line\n{{ with $middleLine }}\nline before value\n{{ . }}\nline after value\n{{ end }}\nnon related line', + expected: [new ExpressionPosition(17, 92)], + }, + { + name: 'newlines: does not match newlines before', + code: '\n{{ with $unimportant }}Text{{ end }}', + expected: [new ExpressionPosition(1, 37)], + }, + { + name: 'newlines: does not match newlines after', + code: '{{ with $unimportant }}Text{{ end }}\n', + expected: [new ExpressionPosition(0, 36)], + }, + ); + }); + describe('throws with incorrect `with` syntax', () => { + runner.expectThrows( + { + name: 'incorrect `with`: whitespace after dollar sign inside `with` statement', + code: '{{with $ parameter}}value: {{ . }}{{ end }}', + expectedError: 'Context variable before `with` statement.', + }, + { + name: 'incorrect `with`: whitespace before dollar sign inside `with` statement', + code: '{{ with$parameter}}value: {{ . }}{{ end }}', + expectedError: 'Context variable before `with` statement.', + }, + { + name: 'incorrect `with`: missing `with` statement', + code: '{{ when $parameter}}value: {{ . }}{{ end }}', + expectedError: 'Context variable before `with` statement.', + }, + { + name: 'incorrect `end`: missing `end` statement', + code: '{{ with $parameter}}value: {{ . }}{{ fin }}', + expectedError: 'Missing `end` statement, forgot `{{ end }}?', + }, + { + name: 'incorrect `end`: used without `with`', + code: 'Value {{ end }}', + expectedError: 'Redundant `end` statement, missing `with`?', + }, + { + name: 'incorrect "context variable": used without `with`', + code: 'Value: {{ . }}', + expectedError: 'Context variable before `with` statement.', + }, + ); + }); + describe('ignores when syntax is wrong', () => { + describe('does not render argument if substitution syntax is wrong', () => { + runner.expectResults( + { + name: 'comma used instead of dot', + code: '{{ with $parameter }}Hello {{ , }}{{ end }}', + args: (args) => args + .withArgument('parameter', 'world!'), + expected: ['Hello {{ , }}'], + }, + { + name: 'single brackets instead of double', + code: '{{ with $parameter }}Hello { . }{{ end }}', + args: (args) => args + .withArgument('parameter', 'world!'), + expected: ['Hello { . }'], + }, + { + name: 'double dots instead of single', + code: '{{ with $parameter }}Hello {{ .. }}{{ end }}', + args: (args) => args + .withArgument('parameter', 'world!'), + expected: ['Hello {{ .. }}'], + }, + ); + }); + }); + describe('scope rendering', () => { + describe('conditional rendering based on argument value', () => { + describe('does not render scope', () => { + runner.expectResults( + ...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true }) + .map((testCase) => ({ + name: `does not render when value is "${testCase.valueName}"`, + code: '{{ with $parameter }}dark{{ end }} ', + args: (args) => args + .withArgument('parameter', testCase.absentValue), + expected: [''], + })), + { + name: 'does not render when argument is not provided', + code: '{{ with $parameter }}dark{{ end }}', + args: (args) => args, + expected: [''], + }, + ); + }); + describe('renders scope', () => { + runner.expectResults( + ...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true }) + .map((testCase) => ({ + name: `does not render when value is "${testCase.valueName}"`, + code: '{{ with $parameter }}dark{{ end }} ', + args: (args) => args + .withArgument('parameter', testCase.absentValue), + expected: [''], + })), + { + name: 'does not render when argument is not provided', + code: '{{ with $parameter }}dark{{ end }}', + args: (args) => args, + expected: [''], + }, + { + name: 'renders scope even if value is not used', + code: '{{ with $parameter }}Hello world!{{ end }}', + args: (args) => args + .withArgument('parameter', 'Hello'), + expected: ['Hello world!'], + }, + { + name: 'renders value when it has value', + code: '{{ with $parameter }}{{ . }} world!{{ end }}', + args: (args) => args + .withArgument('parameter', 'Hello'), + expected: ['Hello world!'], + }, + { + name: 'renders value when whitespaces around brackets are missing', + code: '{{ with $parameter }}{{.}} world!{{ end }}', + args: (args) => args + .withArgument('parameter', 'Hello'), + expected: ['Hello world!'], + }, + { + name: 'renders value multiple times when it\'s used multiple times', + code: '{{ with $letterL }}He{{ . }}{{ . }}o wor{{ . }}d!{{ end }}', + args: (args) => args + .withArgument('letterL', 'l'), + expected: ['Hello world!'], + }, + ); + }); + }); + describe('whitespace handling inside scope', () => { + runner.expectResults( + { + name: 'renders value in multi-lined text', + code: '{{ with $middleLine }}line before value\n{{ . }}\nline after value{{ end }}', + args: (args) => args + .withArgument('middleLine', 'value line'), + expected: ['line before value\nvalue line\nline after value'], + }, + { + name: 'renders value around whitespaces in multi-lined text', + code: '{{ with $middleLine }}\nline before value\n{{ . }}\nline after value\t {{ end }}', + args: (args) => args + .withArgument('middleLine', 'value line'), + expected: ['line before value\nvalue line\nline after value'], + }, + { + name: 'does not render trailing whitespace after value', + code: '{{ with $parameter }}{{ . }}! {{ end }}', + args: (args) => args + .withArgument('parameter', 'Hello world'), + expected: ['Hello world!'], + }, + { + name: 'does not render trailing newline after value', + code: '{{ with $parameter }}{{ . }}!\r\n{{ end }}', + args: (args) => args + .withArgument('parameter', 'Hello world'), + expected: ['Hello world!'], + }, + { + name: 'does not render leading newline before value', + code: '{{ with $parameter }}\r\n{{ . }}!{{ end }}', + args: (args) => args + .withArgument('parameter', 'Hello world'), + expected: ['Hello world!'], + }, + { + name: 'does not render leading whitespaces before value', + code: '{{ with $parameter }} {{ . }}!{{ end }}', + args: (args) => args + .withArgument('parameter', 'Hello world'), + expected: ['Hello world!'], + }, + { + name: 'does not render leading newline and whitespaces before value', + code: '{{ with $parameter }}\r\n {{ . }}!{{ end }}', + args: (args) => args + .withArgument('parameter', 'Hello world'), + expected: ['Hello world!'], + }, + ); + }); + describe('nested with statements', () => { + runner.expectResults( + { + name: 'renders nested with statements correctly', + code: '{{ with $outer }}Outer: {{ with $inner }}Inner: {{ . }}{{ end }}, Outer again: {{ . }}{{ end }}', + args: (args) => args + .withArgument('outer', 'OuterValue') + .withArgument('inner', 'InnerValue'), + expected: [ + 'Inner: InnerValue', + 'Outer: {{ with $inner }}Inner: {{ . }}{{ end }}, Outer again: OuterValue', + ], + }, + { + name: 'renders nested with statements with context variables', + code: '{{ with $outer }}{{ with $inner }}{{ . }}{{ . }}{{ end }}{{ . }}{{ end }}', + args: (args) => args + .withArgument('outer', 'O') + .withArgument('inner', 'I'), + expected: [ + 'II', + '{{ with $inner }}{{ . }}{{ . }}{{ end }}O', + ], + }, + ); + }); + }); + describe('pipe behavior', () => { + runner.expectPipeHits({ + codeBuilder: (pipeline) => `{{ with $argument }} {{ .${pipeline}}} {{ end }}`, + parameterName: 'argument', + parameterValue: 'value', + }); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts new file mode 100644 index 00000000..b35f6157 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'vitest'; +import { createFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import type { NonEmptyStringAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import { createParameterNameValidatorStub } from '@tests/unit/shared/Stubs/ParameterNameValidatorStub'; +import type { ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; + +describe('FunctionCallArgument', () => { + describe('createFunctionCallArgument', () => { + describe('parameter name', () => { + it('assigns correctly', () => { + // arrange + const expectedName = 'expected parameter name'; + const context = new TestContext() + .withParameterName(expectedName); + // act + const actualArgument = context.create(); + // assert + const actualName = actualArgument.parameterName; + expect(actualName).toEqual(expectedName); + }); + it('validates parameter name', () => { + // arrange + const validator = createParameterNameValidatorStub(); + const expectedParameterName = 'parameter name expected to be validated'; + const context = new TestContext() + .withParameterName(expectedParameterName) + .withParameterNameValidator(validator.validator); + // act + context.create(); + // assert + expect(validator.validatedNames).to.have.lengthOf(1); + expect(validator.validatedNames).to.include(expectedParameterName); + }); + }); + describe('argument value', () => { + it('assigns correctly', () => { + // arrange + const expectedValue = 'expected argument value'; + const context = new TestContext() + .withArgumentValue(expectedValue); + // act + const actualArgument = context.create(); + // assert + const actualValue = actualArgument.argumentValue; + expect(actualValue).toEqual(expectedValue); + }); + it('validates argument value', () => { + // arrange + const parameterNameInError = 'expected parameter with argument error'; + const expectedArgumentValue = 'argument value to be validated'; + const expectedAssertion: NonEmptyStringAssertion = { + value: expectedArgumentValue, + valueName: `Missing argument value for the parameter "${parameterNameInError}".`, + }; + const typeValidator = new TypeValidatorStub(); + const context = new TestContext() + .withArgumentValue(expectedArgumentValue) + .withParameterName(parameterNameInError) + .withTypeValidator(typeValidator); + // act + context.create(); + // assert + typeValidator.assertNonEmptyString(expectedAssertion); + }); + }); + }); +}); + +class TestContext { + private parameterName = `[${TestContext.name}] default-parameter-name`; + + private argumentValue = `[${TestContext.name}] default-argument-value`; + + private typeValidator: TypeValidator = new TypeValidatorStub(); + + private parameterNameValidator + : ParameterNameValidator = createParameterNameValidatorStub().validator; + + public withParameterName(parameterName: string): this { + this.parameterName = parameterName; + return this; + } + + public withArgumentValue(argumentValue: string): this { + this.argumentValue = argumentValue; + return this; + } + + public withTypeValidator(typeValidator: TypeValidator): this { + this.typeValidator = typeValidator; + return this; + } + + public withParameterNameValidator(parameterNameValidator: ParameterNameValidator): this { + this.parameterNameValidator = parameterNameValidator; + return this; + } + + public create(): ReturnType { + return createFunctionCallArgument( + this.parameterName, + this.argumentValue, + { + typeValidator: this.typeValidator, + validateParameterName: this.parameterNameValidator, + }, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts new file mode 100644 index 00000000..08c333bf --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts @@ -0,0 +1,138 @@ +import { describe, it, expect } from 'vitest'; +import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; +import { FunctionCallArgumentStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentStub'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import type { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; + +describe('FunctionCallArgumentCollection', () => { + describe('addArgument', () => { + it('throws if parameter value is already provided', () => { + // arrange + const duplicateParameterName = 'duplicateParam'; + const errorMessage = `argument value for parameter ${duplicateParameterName} is already provided`; + const arg1 = new FunctionCallArgumentStub().withParameterName(duplicateParameterName); + const arg2 = new FunctionCallArgumentStub().withParameterName(duplicateParameterName); + const sut = new FunctionCallArgumentCollection(); + // act + sut.addArgument(arg1); + const act = () => sut.addArgument(arg2); + // assert + expect(act).to.throw(errorMessage); + }); + }); + describe('getAllParameterNames', () => { + describe('returns as expected', () => { + // arrange + const testCases: ReadonlyArray<{ + readonly description: string; + readonly args: readonly FunctionCallArgument[]; + readonly expectedParameterNames: string[]; + }> = [{ + description: 'no args', + args: [], + expectedParameterNames: [], + }, { + description: 'with some args', + args: [ + new FunctionCallArgumentStub().withParameterName('a-param-name'), + new FunctionCallArgumentStub().withParameterName('b-param-name')], + expectedParameterNames: ['a-param-name', 'b-param-name'], + }]; + for (const testCase of testCases) { + it(testCase.description, () => { + const sut = new FunctionCallArgumentCollection(); + // act + for (const arg of testCase.args) { + sut.addArgument(arg); + } + const actual = sut.getAllParameterNames(); + // assert + expect(actual).to.deep.equal(testCase.expectedParameterNames); + }); + } + }); + }); + describe('getArgument', () => { + describe('throws if parameter name is absent', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing parameter name'; + const sut = new FunctionCallArgumentCollection(); + const parameterName = absentValue; + // act + const act = () => sut.getArgument(parameterName); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + it('throws if argument does not exist', () => { + // arrange + const parameterName = 'nonExistingParam'; + const expectedError = `parameter does not exist: ${parameterName}`; + const sut = new FunctionCallArgumentCollection(); + // act + const act = () => sut.getArgument(parameterName); + // assert + expect(act).to.throw(expectedError); + }); + it('returns argument as expected', () => { + // arrange + const expected = new FunctionCallArgumentStub() + .withParameterName('expectedName') + .withArgumentValue('expectedValue'); + const sut = new FunctionCallArgumentCollection(); + // act + sut.addArgument(expected); + const actual = sut.getArgument(expected.parameterName); + // assert + expect(actual).to.equal(expected); + }); + }); + describe('hasArgument', () => { + describe('throws if parameter name is missing', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing parameter name'; + const parameterName = absentValue; + const sut = new FunctionCallArgumentCollection(); + // act + const act = () => sut.hasArgument(parameterName); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + describe('returns as expected', () => { + // arrange + const testCases = [{ + name: 'argument exists', + parameter: 'existing-parameter-name', + args: [ + new FunctionCallArgumentStub().withParameterName('existing-parameter-name'), + new FunctionCallArgumentStub().withParameterName('unrelated-parameter-name'), + ], + expected: true, + }, + { + name: 'argument does not exist', + parameter: 'not-existing-parameter-name', + args: [ + new FunctionCallArgumentStub().withParameterName('unrelated-parameter-name-b'), + new FunctionCallArgumentStub().withParameterName('unrelated-parameter-name-a'), + ], + expected: false, + }]; + for (const testCase of testCases) { + it(`"${testCase.name}" returns "${testCase.expected}"`, () => { + const sut = new FunctionCallArgumentCollection(); + // act + for (const arg of testCase.args) { + sut.addArgument(arg); + } + const actual = sut.hasArgument(testCase.parameter); + // assert + expect(actual).to.equal(testCase.expected); + }); + } + }); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.spec.ts new file mode 100644 index 00000000..b6551b83 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.spec.ts @@ -0,0 +1,104 @@ +import { expect, describe, it } from 'vitest'; +import { NewlineCodeSegmentMerger } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger'; +import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub'; +import { getAbsentStringTestCases, itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode'; + +describe('NewlineCodeSegmentMerger', () => { + describe('mergeCodeParts', () => { + describe('throws given empty segments', () => { + itEachAbsentCollectionValue((absentValue) => { + // arrange + const expectedError = 'missing segments'; + const segments = absentValue; + const merger = new NewlineCodeSegmentMerger(); + // act + const act = () => merger.mergeCodeParts(segments); + // assert + expect(act).to.throw(expectedError); + }, { excludeUndefined: true, excludeNull: true }); + }); + describe('merges correctly', () => { + const testCases: ReadonlyArray<{ + readonly description: string, + readonly segments: CompiledCodeStub[], + readonly expected: { + readonly code: string, + readonly revertCode?: string, + }, + }> = [ + { + description: 'given `code` and `revertCode`', + segments: [ + new CompiledCodeStub().withCode('code1').withRevertCode('revert1'), + new CompiledCodeStub().withCode('code2').withRevertCode('revert2'), + new CompiledCodeStub().withCode('code3').withRevertCode('revert3'), + ], + expected: { + code: 'code1\ncode2\ncode3', + revertCode: 'revert1\nrevert2\nrevert3', + }, + }, + ...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true }) + .map((absentTestCase) => ({ + description: `filter out ${absentTestCase.valueName} \`revertCode\``, + segments: [ + new CompiledCodeStub().withCode('code1').withRevertCode('revert1'), + new CompiledCodeStub().withCode('code2').withRevertCode(absentTestCase.absentValue), + new CompiledCodeStub().withCode('code3').withRevertCode('revert3'), + ], + expected: { + code: 'code1\ncode2\ncode3', + revertCode: 'revert1\nrevert3', + }, + })), + ...getAbsentStringTestCases({ excludeNull: true }) + .map((emptyRevertCode) => ({ + description: `given only \`code\` in segments with "${emptyRevertCode.valueName}" \`revertCode\``, + segments: [ + new CompiledCodeStub().withCode('code1').withRevertCode(emptyRevertCode.absentValue), + new CompiledCodeStub().withCode('code2').withRevertCode(emptyRevertCode.absentValue), + ], + expected: { + code: 'code1\ncode2', + revertCode: '', + }, + })), + { + description: 'given mix of segments with only `code` or `revertCode`', + segments: [ + new CompiledCodeStub().withCode('code1').withRevertCode(''), + new CompiledCodeStub().withCode('').withRevertCode('revert2'), + new CompiledCodeStub().withCode('code3').withRevertCode(''), + ], + expected: { + code: 'code1\ncode3', + revertCode: 'revert2', + }, + }, + { + description: 'given only `revertCode` in segments', + segments: [ + new CompiledCodeStub().withCode('').withRevertCode('revert1'), + new CompiledCodeStub().withCode('').withRevertCode('revert2'), + ], + expected: { + code: '', + revertCode: 'revert1\nrevert2', + }, + }, + ]; + for (const { segments, expected, description } of testCases) { + it(description, () => { + // arrange + const merger = new NewlineCodeSegmentMerger(); + // act + const actual = merger.mergeCodeParts(segments); + // assert + expect(actual.code).to.equal(expected.code); + expect(actual.revertCode).to.equal(expected.revertCode); + }); + } + }); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.spec.ts new file mode 100644 index 00000000..06333270 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.spec.ts @@ -0,0 +1,223 @@ +/* eslint-disable max-classes-per-file */ +import { describe, it, expect } from 'vitest'; +import { FunctionCallSequenceCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler'; +import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests'; +import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import type { SingleCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler'; +import type { CodeSegmentMerger } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger'; +import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection'; +import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub'; +import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStub'; +import { CodeSegmentMergerStub } from '@tests/unit/shared/Stubs/CodeSegmentMergerStub'; +import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub'; +import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; + +describe('FunctionCallSequenceCompiler', () => { + describe('instance', () => { + itIsSingletonFactory({ + getter: () => FunctionCallSequenceCompiler.instance, + expectedType: FunctionCallSequenceCompiler, + }); + }); + describe('compileFunctionCalls', () => { + describe('parameter validation', () => { + describe('calls', () => { + describe('throws with missing call', () => { + itEachAbsentCollectionValue((absentValue) => { + // arrange + const expectedError = 'missing calls'; + const calls = absentValue; + const builder = new FunctionCallSequenceCompilerBuilder() + .withCalls(calls); + // act + const act = () => builder.compileFunctionCalls(); + // assert + expect(act).to.throw(expectedError); + }, { excludeUndefined: true, excludeNull: true }); + }); + }); + }); + describe('invokes single call compiler correctly', () => { + describe('calls', () => { + it('with expected call', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedCall = new FunctionCallStub(); + const builder = new FunctionCallSequenceCompilerBuilder() + .withSingleCallCompiler(singleCallCompilerStub) + .withCalls([expectedCall]); + // act + builder.compileFunctionCalls(); + // assert + expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1); + const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall'); + expectExists(calledMethod); + expect(calledMethod.args[0]).to.equal(expectedCall); + }); + it('with every call', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedCalls = [ + new FunctionCallStub(), new FunctionCallStub(), new FunctionCallStub(), + ]; + const builder = new FunctionCallSequenceCompilerBuilder() + .withSingleCallCompiler(singleCallCompilerStub) + .withCalls(expectedCalls); + // act + builder.compileFunctionCalls(); + // assert + const calledMethods = singleCallCompilerStub.callHistory.filter((m) => m.methodName === 'compileSingleCall'); + expect(calledMethods).to.have.lengthOf(expectedCalls.length); + const callArguments = calledMethods.map((c) => c.args[0]); + expect(expectedCalls).to.have.members(callArguments); + }); + }); + describe('context', () => { + it('with expected functions', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedFunctions = new SharedFunctionCollectionStub(); + const builder = new FunctionCallSequenceCompilerBuilder() + .withSingleCallCompiler(singleCallCompilerStub) + .withFunctions(expectedFunctions); + // act + builder.compileFunctionCalls(); + // assert + expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1); + const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall'); + expectExists(calledMethod); + const actualFunctions = calledMethod.args[1].allFunctions; + expect(actualFunctions).to.equal(expectedFunctions); + }); + it('with expected call sequence', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedCallSequence = [new FunctionCallStub(), new FunctionCallStub()]; + const builder = new FunctionCallSequenceCompilerBuilder() + .withSingleCallCompiler(singleCallCompilerStub) + .withCalls(expectedCallSequence); + // act + builder.compileFunctionCalls(); + // assert + const calledMethods = singleCallCompilerStub.callHistory.filter((m) => m.methodName === 'compileSingleCall'); + expect(calledMethods).to.have.lengthOf(expectedCallSequence.length); + const calledSequenceArgs = calledMethods.map((call) => call.args[1].rootCallSequence); + expect(calledSequenceArgs.every((sequence) => sequence === expectedCallSequence)); + }); + it('with expected call compiler', () => { + // arrange + const expectedCompiler = new SingleCallCompilerStub(); + const rootCallSequence = [new FunctionCallStub(), new FunctionCallStub()]; + const builder = new FunctionCallSequenceCompilerBuilder() + .withCalls(rootCallSequence) + .withSingleCallCompiler(expectedCompiler); + // act + builder.compileFunctionCalls(); + // assert + const calledMethods = expectedCompiler.callHistory.filter((m) => m.methodName === 'compileSingleCall'); + expect(calledMethods).to.have.lengthOf(rootCallSequence.length); + const compilerArgs = calledMethods.map((call) => call.args[1].singleCallCompiler); + expect(compilerArgs.every((compiler) => compiler === expectedCompiler)); + }); + }); + }); + describe('code segment merger', () => { + it('invokes code segment merger correctly', () => { + // arrange + const singleCallCompilationScenario = new Map([ + [new FunctionCallStub(), [new CompiledCodeStub()]], + [new FunctionCallStub(), [new CompiledCodeStub(), new CompiledCodeStub()]], + ]); + const expectedFlattenedSegments = [...singleCallCompilationScenario.values()].flat(); + const calls = [...singleCallCompilationScenario.keys()]; + const singleCallCompiler = new SingleCallCompilerStub() + .withCallCompilationScenarios(singleCallCompilationScenario); + const codeSegmentMergerStub = new CodeSegmentMergerStub(); + const builder = new FunctionCallSequenceCompilerBuilder() + .withCalls(calls) + .withSingleCallCompiler(singleCallCompiler) + .withCodeSegmentMerger(codeSegmentMergerStub); + // act + builder.compileFunctionCalls(); + // assert + const calledMethod = codeSegmentMergerStub.callHistory.find((c) => c.methodName === 'mergeCodeParts'); + expectExists(calledMethod); + const [actualSegments] = calledMethod.args; + expect(expectedFlattenedSegments).to.have.lengthOf(actualSegments.length); + expect(expectedFlattenedSegments).to.have.deep.members(actualSegments); + }); + it('returns code segment merger result', () => { + // arrange + const expectedResult = new CompiledCodeStub(); + const codeSegmentMergerStub = new CodeSegmentMergerStub(); + codeSegmentMergerStub.mergeCodeParts = () => expectedResult; + const builder = new FunctionCallSequenceCompilerBuilder() + .withCodeSegmentMerger(codeSegmentMergerStub); + // act + const actualResult = builder.compileFunctionCalls(); + // assert + expect(actualResult).to.equal(expectedResult); + }); + }); + }); +}); + +class FunctionCallSequenceCompilerBuilder { + private singleCallCompiler: SingleCallCompiler = new SingleCallCompilerStub(); + + private codeSegmentMerger: CodeSegmentMerger = new CodeSegmentMergerStub(); + + private functions: ISharedFunctionCollection = new SharedFunctionCollectionStub(); + + private calls: readonly FunctionCall[] = [ + new FunctionCallStub(), + ]; + + public withSingleCallCompiler(compiler: SingleCallCompiler): this { + this.singleCallCompiler = compiler; + return this; + } + + public withCodeSegmentMerger(merger: CodeSegmentMerger): this { + this.codeSegmentMerger = merger; + return this; + } + + public withCalls(calls: readonly FunctionCall[]): this { + this.calls = calls; + return this; + } + + public withFunctions(functions: ISharedFunctionCollection): this { + this.functions = functions; + return this; + } + + public compileFunctionCalls() { + const compiler = new TestableFunctionCallSequenceCompiler({ + singleCallCompiler: this.singleCallCompiler, + codeSegmentMerger: this.codeSegmentMerger, + }); + return compiler.compileFunctionCalls( + this.calls, + this.functions, + ); + } +} + +interface FunctionCallSequenceCompilerStubs { + readonly singleCallCompiler?: SingleCallCompiler; + readonly codeSegmentMerger: CodeSegmentMerger; +} + +class TestableFunctionCallSequenceCompiler extends FunctionCallSequenceCompiler { + public constructor(options: FunctionCallSequenceCompilerStubs) { + super( + options.singleCallCompiler, + options.codeSegmentMerger, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/NestedFunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/NestedFunctionCallCompiler.spec.ts new file mode 100644 index 00000000..ba5df9e3 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/NestedFunctionCallCompiler.spec.ts @@ -0,0 +1,294 @@ +import { expect, describe, it } from 'vitest'; +import { createSharedFunctionStubWithCalls, createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub'; +import { NestedFunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler'; +import type { ArgumentCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler'; +import { ArgumentCompilerStub } from '@tests/unit/shared/Stubs/ArgumentCompilerStub'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub'; +import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStub'; +import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub'; +import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; +import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; +import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub'; + +describe('NestedFunctionCallCompiler', () => { + describe('canCompile', () => { + it('returns `true` for code body function', () => { + // arrange + const expected = true; + const func = createSharedFunctionStubWithCalls() + .withSomeCalls(); + const compiler = new NestedFunctionCallCompilerBuilder() + .build(); + // act + const actual = compiler.canCompile(func); + // assert + expect(actual).to.equal(expected); + }); + it('returns `false` for non-code body function', () => { + // arrange + const expected = false; + const func = createSharedFunctionStubWithCode(); + const compiler = new NestedFunctionCallCompilerBuilder() + .build(); + // act + const actual = compiler.canCompile(func); + // assert + expect(actual).to.equal(expected); + }); + }); + describe('compile', () => { + describe('argument compilation', () => { + it('uses correct context', () => { + // arrange + const argumentCompiler = new ArgumentCompilerStub(); + const expectedContext = new FunctionCallCompilationContextStub(); + const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompiler) + .build(); + // act + compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext); + // assert + const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall'); + expect(calls).have.lengthOf(1); + const [,,actualContext] = calls[0].args; + expect(actualContext).to.equal(expectedContext); + }); + it('uses correct parent call', () => { + // arrange + const argumentCompiler = new ArgumentCompilerStub(); + const expectedContext = new FunctionCallCompilationContextStub(); + const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const expectedParentCall = callToFrontFunc; + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompiler) + .build(); + // act + compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext); + // assert + const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall'); + expect(calls).have.lengthOf(1); + const [,actualParentCall] = calls[0].args; + expect(actualParentCall).to.equal(expectedParentCall); + }); + it('uses correct nested call', () => { + // arrange + const argumentCompiler = new ArgumentCompilerStub(); + const expectedContext = new FunctionCallCompilationContextStub(); + const { + frontFunction, callToDeepFunc, callToFrontFunc, + } = createSingleFuncCallingAnotherFunc(); + const expectedNestedCall = callToDeepFunc; + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompiler) + .build(); + // act + compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext); + // assert + const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall'); + expect(calls).have.lengthOf(1); + const [actualNestedCall] = calls[0].args; + expect(actualNestedCall).to.deep.equal(expectedNestedCall); + }); + }); + describe('re-compilation with compiled args', () => { + it('uses correct context', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedContext = new FunctionCallCompilationContextStub() + .withSingleCallCompiler(singleCallCompilerStub); + const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const compiler = new NestedFunctionCallCompilerBuilder() + .build(); + // act + compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext); + // assert + const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall'); + expect(calls).have.lengthOf(1); + const [,actualContext] = calls[0].args; + expect(expectedContext).to.equal(actualContext); + }); + it('uses compiled nested call', () => { + // arrange + const expectedCall = new FunctionCallStub(); + const argumentCompilerStub = new ArgumentCompilerStub(); + argumentCompilerStub.createCompiledNestedCall = () => expectedCall; + const singleCallCompilerStub = new SingleCallCompilerStub(); + const context = new FunctionCallCompilationContextStub() + .withSingleCallCompiler(singleCallCompilerStub); + const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompilerStub) + .build(); + // act + compiler.compileFunction(frontFunction, callToFrontFunc, context); + // assert + const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall'); + expect(calls).have.lengthOf(1); + const [actualNestedCall] = calls[0].args; + expect(expectedCall).to.equal(actualNestedCall); + }); + }); + it('flattens re-compiled functions', () => { + // arrange + const deepFunc1 = createSharedFunctionStubWithCode(); + const deepFunc2 = createSharedFunctionStubWithCalls(); + const callToDeepFunc1 = new FunctionCallStub().withFunctionName(deepFunc1.name); + const callToDeepFunc2 = new FunctionCallStub().withFunctionName(deepFunc2.name); + const singleCallCompilationScenario = new Map([ + [callToDeepFunc1, [new CompiledCodeStub()]], + [callToDeepFunc2, [new CompiledCodeStub(), new CompiledCodeStub()]], + ]); + const argumentCompiler = new ArgumentCompilerStub() + .withScenario({ givenNestedFunctionCall: callToDeepFunc1, result: callToDeepFunc1 }) + .withScenario({ givenNestedFunctionCall: callToDeepFunc2, result: callToDeepFunc2 }); + const expectedFlattenedCodes = [...singleCallCompilationScenario.values()].flat(); + const frontFunction = createSharedFunctionStubWithCalls() + .withCalls(callToDeepFunc1, callToDeepFunc2); + const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunction.name); + const singleCallCompilerStub = new SingleCallCompilerStub() + .withCallCompilationScenarios(singleCallCompilationScenario); + const expectedContext = new FunctionCallCompilationContextStub() + .withSingleCallCompiler(singleCallCompilerStub); + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompiler) + .build(); + // act + const actualCodes = compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext); + // assert + expect(actualCodes).have.lengthOf(expectedFlattenedCodes.length); + expect(actualCodes).to.have.members(expectedFlattenedCodes); + }); + describe('error handling', () => { + describe('rethrows error from argument compiler', () => { + // arrange + const expectedInnerError = new Error(`Expected error from ${ArgumentCompilerStub.name}`); + const calleeFunctionName = 'expectedCalleeFunctionName'; + const callerFunctionName = 'expectedCallerFunctionName'; + const expectedErrorMessage = buildRethrowErrorMessage({ + callee: calleeFunctionName, + caller: callerFunctionName, + }); + const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc({ + frontFunctionName: callerFunctionName, + deepFunctionName: calleeFunctionName, + }); + const argumentCompilerStub = new ArgumentCompilerStub(); + argumentCompilerStub.createCompiledNestedCall = () => { + throw expectedInnerError; + }; + const builder = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompilerStub); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + builder + .withErrorWrapper(wrapError) + .build() + .compileFunction( + frontFunction, + callToFrontFunc, + new FunctionCallCompilationContextStub(), + ); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + describe('rethrows error from single call compiler', () => { + // arrange + const expectedInnerError = new Error(`Expected error from ${SingleCallCompilerStub.name}`); + const calleeFunctionName = 'expectedCalleeFunctionName'; + const callerFunctionName = 'expectedCallerFunctionName'; + const expectedErrorMessage = buildRethrowErrorMessage({ + callee: calleeFunctionName, + caller: callerFunctionName, + }); + const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc({ + frontFunctionName: callerFunctionName, + deepFunctionName: calleeFunctionName, + }); + const singleCallCompiler = new SingleCallCompilerStub(); + singleCallCompiler.compileSingleCall = () => { + throw expectedInnerError; + }; + const context = new FunctionCallCompilationContextStub() + .withSingleCallCompiler(singleCallCompiler); + const builder = new NestedFunctionCallCompilerBuilder(); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + builder + .withErrorWrapper(wrapError) + .build() + .compileFunction( + frontFunction, + callToFrontFunc, + context, + ); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + }); + }); +}); + +function createSingleFuncCallingAnotherFunc( + functionNames?: { + readonly frontFunctionName?: string; + readonly deepFunctionName?: string; + }, +) { + const deepFunction = createSharedFunctionStubWithCode() + .withName(functionNames?.deepFunctionName ?? 'deep-function (is called by front-function)'); + const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunction.name); + const frontFunction = createSharedFunctionStubWithCalls() + .withCalls(callToDeepFunc) + .withName(functionNames?.frontFunctionName ?? 'front-function (calls deep-function)'); + const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunction.name); + return { + deepFunction, + frontFunction, + callToFrontFunc, + callToDeepFunc, + }; +} + +class NestedFunctionCallCompilerBuilder { + private argumentCompiler: ArgumentCompiler = new ArgumentCompilerStub(); + + private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub; + + public withArgumentCompiler(argumentCompiler: ArgumentCompiler): this { + this.argumentCompiler = argumentCompiler; + return this; + } + + public withErrorWrapper(wrapError: ErrorWithContextWrapper): this { + this.wrapError = wrapError; + return this; + } + + public build(): NestedFunctionCallCompiler { + return new NestedFunctionCallCompiler( + this.argumentCompiler, + this.wrapError, + ); + } +} + +function buildRethrowErrorMessage( + functionNames: { + readonly caller: string; + readonly callee: string; + }, +) { + return `Failed to call '${functionNames.callee}' (callee function) from '${functionNames.caller}' (caller function).`; +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/AdaptiveFunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/AdaptiveFunctionCallCompiler.spec.ts new file mode 100644 index 00000000..3c817e74 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/AdaptiveFunctionCallCompiler.spec.ts @@ -0,0 +1,260 @@ +import { expect, describe, it } from 'vitest'; +import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub'; +import type { FunctionCallParametersData } from '@/application/collections/'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub'; +import { AdaptiveFunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/AdaptiveFunctionCallCompiler'; +import type { SingleCallCompilerStrategy } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy'; +import { SingleCallCompilerStrategyStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStrategyStub'; +import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; +import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub'; +import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub'; +import type { SingleCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler'; +import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector'; + +describe('AdaptiveFunctionCallCompiler', () => { + describe('compileSingleCall', () => { + describe('throws if call parameters does not match function parameters', () => { + // arrange + const functionName = 'test-function-name'; + const testCases: Array<{ + readonly description: string, + readonly functionParameters: string[], + readonly callParameters: string[] + readonly expectedError: string; + }> = [ + { + description: 'provided: single unexpected parameter, when: another expected', + functionParameters: ['expected-parameter'], + callParameters: ['unexpected-parameter'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".` + + '\nExpected parameter(s): "expected-parameter"', + }, + { + description: 'provided: multiple unexpected parameters, when: different one is expected', + functionParameters: ['expected-parameter'], + callParameters: ['unexpected-parameter1', 'unexpected-parameter2'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2".` + + '\nExpected parameter(s): "expected-parameter"', + }, + { + description: 'provided: an unexpected parameter, when: multiple parameters are expected', + functionParameters: ['expected-parameter1', 'expected-parameter2'], + callParameters: ['expected-parameter1', 'expected-parameter2', 'unexpected-parameter'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".` + + '\nExpected parameter(s): "expected-parameter1", "expected-parameter2"', + }, + { + description: 'provided: an unexpected parameter, when: none required', + functionParameters: [], + callParameters: ['unexpected-call-parameter'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter".` + + '\nExpected parameter(s): none', + }, + { + description: 'provided: expected and unexpected parameter, when: one of them is expected', + functionParameters: ['expected-parameter'], + callParameters: ['expected-parameter', 'unexpected-parameter'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".` + + '\nExpected parameter(s): "expected-parameter"', + }, + ]; + testCases.forEach(({ + description, functionParameters, callParameters, expectedError, + }) => { + it(description, () => { + // arrange + const func = createSharedFunctionStubWithCode() + .withName('test-function-name') + .withParameterNames(...functionParameters); + const params = callParameters + .reduce((result, parameter) => { + return { ...result, [parameter]: 'defined-parameter-value' }; + }, {} as FunctionCallParametersData); + const call = new FunctionCallStub() + .withFunctionName(func.name) + .withArguments(params); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withContext(new FunctionCallCompilationContextStub() + .withAllFunctions( + new SharedFunctionCollectionStub().withFunctions(func), + )) + .withCall(call); + // act + const act = () => builder.compileSingleCall(); + // assert + const errorMessage = collectExceptionMessage(act); + expect(errorMessage).to.include(expectedError); + }); + }); + }); + describe('strategy selection', () => { + it('uses the matching strategy among multiple', () => { + // arrange + const matchedStrategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const unmatchedStrategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(false); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([matchedStrategy, unmatchedStrategy]); + // act + builder.compileSingleCall(); + // assert + expect(matchedStrategy.callHistory.filter((c) => c.methodName === 'compileFunction')).to.have.lengthOf(1); + expect(unmatchedStrategy.callHistory.filter((c) => c.methodName === 'compileFunction')).to.have.lengthOf(0); + }); + it('throws if multiple strategies can compile', () => { + // arrange + const expectedError = 'Multiple strategies found to compile the function call.'; + const matchedStrategy1 = new SingleCallCompilerStrategyStub().withCanCompileResult(true); + const matchedStrategy2 = new SingleCallCompilerStrategyStub().withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder().withStrategies( + [matchedStrategy1, matchedStrategy2], + ); + // act + const act = () => builder.compileSingleCall(); + // assert + expect(act).to.throw(expectedError); + }); + it('throws if no strategy can compile', () => { + // arrange + const expectedError = 'No strategies found to compile the function call.'; + const unmatchedStrategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(false); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([unmatchedStrategy]); + // act + const act = () => builder.compileSingleCall(); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('strategy invocation', () => { + it('passes correct function for compilation ability check', () => { + // arrange + const expectedFunction = createSharedFunctionStubWithCode(); + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withContext(new FunctionCallCompilationContextStub() + .withAllFunctions( + new SharedFunctionCollectionStub().withFunctions(expectedFunction), + )) + .withCall(new FunctionCallStub().withFunctionName(expectedFunction.name)) + .withStrategies([strategy]); + // act + builder.compileSingleCall(); + // assert + const call = strategy.callHistory.filter((c) => c.methodName === 'canCompile'); + expect(call).to.have.lengthOf(1); + expect(call[0].args[0]).to.equal(expectedFunction); + }); + describe('compilation arguments', () => { + it('uses correct function', () => { + // arrange + const expectedFunction = createSharedFunctionStubWithCode(); + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withContext(new FunctionCallCompilationContextStub() + .withAllFunctions( + new SharedFunctionCollectionStub().withFunctions(expectedFunction), + )) + .withCall(new FunctionCallStub().withFunctionName(expectedFunction.name)) + .withStrategies([strategy]); + // act + builder.compileSingleCall(); + // assert + const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction'); + expect(call).to.have.lengthOf(1); + const [actualFunction] = call[0].args; + expect(actualFunction).to.equal(expectedFunction); + }); + it('uses correct call', () => { + // arrange + const expectedCall = new FunctionCallStub(); + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([strategy]) + .withCall(expectedCall); + // act + builder.compileSingleCall(); + // assert + const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction'); + expect(call).to.have.lengthOf(1); + const [,actualCall] = call[0].args; + expect(actualCall).to.equal(expectedCall); + }); + it('uses correct context', () => { + // arrange + const expectedContext = new FunctionCallCompilationContextStub(); + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([strategy]) + .withContext(expectedContext); + // act + builder.compileSingleCall(); + // assert + const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction'); + expect(call).to.have.lengthOf(1); + const [,,actualContext] = call[0].args; + expect(actualContext).to.equal(expectedContext); + }); + }); + }); + it('returns compiled code from strategy', () => { + // arrange + const expectedResult = [new CompiledCodeStub(), new CompiledCodeStub()]; + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true) + .withCompiledFunctionResult(expectedResult); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([strategy]); + // act + const actualResult = builder.compileSingleCall(); + // assert + expect(expectedResult).to.equal(actualResult); + }); + }); +}); + +class AdaptiveFunctionCallCompilerBuilder implements SingleCallCompiler { + private strategies: SingleCallCompilerStrategy[] = [ + new SingleCallCompilerStrategyStub().withCanCompileResult(true), + ]; + + private call: FunctionCall = new FunctionCallStub(); + + private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub(); + + public withCall(call: FunctionCall): this { + this.call = call; + return this; + } + + public withContext(context: FunctionCallCompilationContext): this { + this.context = context; + return this; + } + + public withStrategies(strategies: SingleCallCompilerStrategy[]): this { + this.strategies = strategies; + return this; + } + + public compileSingleCall() { + const compiler = new AdaptiveFunctionCallCompiler(this.strategies); + return compiler.compileSingleCall( + this.call, + this.context, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts new file mode 100644 index 00000000..868cb138 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts @@ -0,0 +1,311 @@ +import { expect, describe, it } from 'vitest'; +import type { ArgumentCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler'; +import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; +import { NestedFunctionArgumentCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler'; +import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler'; +import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub'; +import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; +import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub'; +import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub'; +import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub'; +import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; +import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub'; +import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { FunctionCallArgumentFactoryStub } from '../../../../../../../../../../../shared/Stubs/FunctionCallArgumentFactoryStub'; + +describe('NestedFunctionArgumentCompiler', () => { + describe('createCompiledNestedCall', () => { + describe('rethrows error from expressions compiler', () => { + // arrange + const expectedInnerError = new Error('child-'); + const parameterName = 'parameterName'; + const expectedErrorMessage = `Error when compiling argument for "${parameterName}"`; + const nestedCall = new FunctionCallStub() + .withFunctionName('nested-function-call') + .withArgumentCollection(new FunctionCallArgumentCollectionStub() + .withArgument(parameterName, 'unimportant-value')); + const parentCall = new FunctionCallStub() + .withFunctionName('parent-function-call'); + const expressionsCompiler = new ExpressionsCompilerStub(); + expressionsCompiler.compileExpressions = () => { throw expectedInnerError; }; + const builder = new NestedFunctionArgumentCompilerBuilder() + .withParentFunctionCall(parentCall) + .withNestedFunctionCall(nestedCall) + .withExpressionsCompiler(expressionsCompiler); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + builder + .withErrorWrapper(wrapError) + .createCompiledNestedCall(); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + describe('compilation', () => { + describe('without arguments', () => { + it('matches nested call name', () => { + // arrange + const expectedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments()); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(expectedCall); + // act + const actualCall = builder.createCompiledNestedCall(); + // assert + expect(actualCall.functionName).to.equal(expectedCall.functionName); + }); + it('has no arguments or parameters', () => { + // arrange + const expectedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments()); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(expectedCall); + // act + const actualCall = builder.createCompiledNestedCall(); + // assert + expect(actualCall.args.getAllParameterNames()).to.have.lengthOf(0); + }); + it('does not compile expressions', () => { + // arrange + const expressionsCompilerStub = new ExpressionsCompilerStub(); + const call = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments()); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(call) + .withExpressionsCompiler(expressionsCompilerStub); + // act + builder.createCompiledNestedCall(); + // assert + expect(expressionsCompilerStub.callHistory).to.have.lengthOf(0); + }); + }); + describe('with arguments', () => { + it('matches nested call name', () => { + // arrange + const expectedName = 'expected-nested-function-call-name'; + const nestedCall = new FunctionCallStub() + .withFunctionName(expectedName) + .withArgumentCollection(new FunctionCallArgumentCollectionStub().withSomeArguments()); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(nestedCall); + // act + const call = builder.createCompiledNestedCall(); + // assert + expect(call.functionName).to.equal(expectedName); + }); + it('matches nested call parameters', () => { + // arrange + const expectedParameterNames = ['expectedFirstParameterName', 'expectedSecondParameterName']; + const nestedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub() + .withArguments(expectedParameterNames.reduce((acc, name) => ({ ...acc, ...{ [name]: 'unimportant-value' } }), {}))); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(nestedCall); + // act + const call = builder.createCompiledNestedCall(); + // assert + const actualParameterNames = call.args.getAllParameterNames(); + expect(actualParameterNames.length).to.equal(expectedParameterNames.length); + expect(actualParameterNames).to.have.members(expectedParameterNames); + }); + it('compiles args using parent parameters', () => { + // arrange + const expressionsCompilerStub = new ExpressionsCompilerStub(); + const testParameterScenarios = [ + { + parameterName: 'firstParameterName', + rawArgumentValue: 'first-raw-argument-value', + compiledArgumentValue: 'first-compiled-argument-value', + }, + { + parameterName: 'secondParameterName', + rawArgumentValue: 'second-raw-argument-value', + compiledArgumentValue: 'second-compiled-argument-value', + }, + ]; + const parentCall = new FunctionCallStub().withArgumentCollection( + new FunctionCallArgumentCollectionStub().withSomeArguments(), + ); + testParameterScenarios.forEach(({ rawArgumentValue }) => { + expressionsCompilerStub.setup({ + givenCode: rawArgumentValue, + givenArgs: parentCall.args, + result: testParameterScenarios.find( + (r) => r.rawArgumentValue === rawArgumentValue, + )?.compiledArgumentValue ?? 'unexpected arguments', + }); + }); + const nestedCallArgs = new FunctionCallArgumentCollectionStub() + .withArguments(testParameterScenarios.reduce(( + acc, + { parameterName, rawArgumentValue }, + ) => ({ ...acc, ...{ [parameterName]: rawArgumentValue } }), {})); + const nestedCall = new FunctionCallStub() + .withArgumentCollection(nestedCallArgs); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .withParentFunctionCall(parentCall) + .withNestedFunctionCall(nestedCall); + // act + const compiledCall = builder.createCompiledNestedCall(); + // assert + const expectedParameterNames = testParameterScenarios.map((p) => p.parameterName); + const actualParameterNames = compiledCall.args.getAllParameterNames(); + expect(expectedParameterNames.length).to.equal(actualParameterNames.length); + expect(expectedParameterNames).to.have.members(actualParameterNames); + const getActualArgumentValue = (parameterName: string) => compiledCall + .args + .getArgument(parameterName) + .argumentValue; + testParameterScenarios.forEach(({ parameterName, compiledArgumentValue }) => { + expect(getActualArgumentValue(parameterName)).to.equal(compiledArgumentValue); + }); + }); + describe('when expression compiler returns empty', () => { + it('throws for required parameter', () => { + // arrange + const parameterName = 'requiredParameter'; + const initialValue = 'initial-value'; + const emptyCompiledExpression = ''; + const expectedError = `Compilation resulted in empty value for required parameter: "${parameterName}"`; + const nestedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub() + .withArgument(parameterName, initialValue)); + const parentCall = new FunctionCallStub().withArgumentCollection( + new FunctionCallArgumentCollectionStub().withSomeArguments(), + ); + const context = createContextWithParameter({ + existingFunctionName: nestedCall.functionName, + existingParameterName: parameterName, + isExistingParameterOptional: false, + }); + const expressionsCompilerStub = new ExpressionsCompilerStub() + .setup({ + givenCode: initialValue, + givenArgs: parentCall.args, + result: emptyCompiledExpression, + }); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .withParentFunctionCall(parentCall) + .withContext(context) + .withNestedFunctionCall(nestedCall); + // act + const act = () => builder.createCompiledNestedCall(); + // assert + expect(act).to.throw(expectedError); + }); + it('succeeds for optional parameter', () => { + // arrange + const parameterName = 'optionalParameter'; + const initialValue = 'initial-value'; + const emptyValue = ''; + const nestedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub() + .withArgument(parameterName, initialValue)); + const parentCall = new FunctionCallStub().withArgumentCollection( + new FunctionCallArgumentCollectionStub().withSomeArguments(), + ); + const context = createContextWithParameter({ + existingFunctionName: nestedCall.functionName, + existingParameterName: parameterName, + isExistingParameterOptional: true, + }); + const expressionsCompilerStub = new ExpressionsCompilerStub() + .setup({ + givenCode: initialValue, + givenArgs: parentCall.args, + result: emptyValue, + }); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .withParentFunctionCall(parentCall) + .withContext(context) + .withNestedFunctionCall(nestedCall); + // act + const compiledCall = builder.createCompiledNestedCall(); + // assert + expect(compiledCall.args.hasArgument(parameterName)).toBeFalsy(); + }); + }); + }); + }); + }); +}); + +function createContextWithParameter(options: { + readonly existingFunctionName: string, + readonly existingParameterName: string, + readonly isExistingParameterOptional: boolean, +}): FunctionCallCompilationContext { + const parameters = new FunctionParameterCollectionStub() + .withParameterName(options.existingParameterName, options.isExistingParameterOptional); + const func = createSharedFunctionStubWithCode() + .withName(options.existingFunctionName) + .withParameters(parameters); + const functions = new SharedFunctionCollectionStub() + .withFunctions(func); + const context = new FunctionCallCompilationContextStub() + .withAllFunctions(functions); + return context; +} + +class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler { + private expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub(); + + private nestedFunctionCall: FunctionCall = new FunctionCallStub(); + + private parentFunctionCall: FunctionCall = new FunctionCallStub(); + + private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub(); + + private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub; + + private callArgumentFactory + : FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory; + + public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this { + this.expressionsCompiler = expressionsCompiler; + return this; + } + + public withParentFunctionCall(parentFunctionCall: FunctionCall): this { + this.parentFunctionCall = parentFunctionCall; + return this; + } + + public withNestedFunctionCall(nestedFunctionCall: FunctionCall): this { + this.nestedFunctionCall = nestedFunctionCall; + return this; + } + + public withContext(context: FunctionCallCompilationContext): this { + this.context = context; + return this; + } + + public withErrorWrapper(wrapError: ErrorWithContextWrapper): this { + this.wrapError = wrapError; + return this; + } + + public createCompiledNestedCall(): FunctionCall { + const compiler = new NestedFunctionArgumentCompiler({ + expressionsCompiler: this.expressionsCompiler, + wrapError: this.wrapError, + createCallArgument: this.callArgumentFactory, + }); + return compiler.createCompiledNestedCall( + this.nestedFunctionCall, + this.parentFunctionCall, + this.context, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.spec.ts new file mode 100644 index 00000000..fa31682f --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.spec.ts @@ -0,0 +1,145 @@ +import { expect, describe, it } from 'vitest'; +import { InlineFunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler'; +import { createSharedFunctionStubWithCode, createSharedFunctionStubWithCalls } from '@tests/unit/shared/Stubs/SharedFunctionStub'; +import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub'; +import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; + +describe('InlineFunctionCallCompiler', () => { + describe('canCompile', () => { + it('returns `true` if function has code body', () => { + // arrange + const expected = true; + const func = createSharedFunctionStubWithCode(); + const compiler = new InlineFunctionCallCompilerBuilder() + .build(); + // act + const actual = compiler.canCompile(func); + // assert + expect(actual).to.equal(expected); + }); + it('returns `false` if function does not have code body', () => { + // arrange + const expected = false; + const func = createSharedFunctionStubWithCalls(); + const compiler = new InlineFunctionCallCompilerBuilder() + .build(); + // act + const actual = compiler.canCompile(func); + // assert + expect(actual).to.equal(expected); + }); + }); + describe('compile', () => { + it('throws if function body is not code', () => { + // arrange + const expectedError = 'Unexpected function body type.'; + const compiler = new InlineFunctionCallCompilerBuilder() + .build(); + // act + const act = () => compiler.compileFunction( + createSharedFunctionStubWithCalls(), + new FunctionCallStub(), + ); + // assert + expect(act).to.throw(expectedError); + }); + it('compiles expressions with correct arguments', () => { + // arrange + const expressionsCompilerStub = new ExpressionsCompilerStub(); + const expectedArgs = new FunctionCallArgumentCollectionStub(); + const compiler = new InlineFunctionCallCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .build(); + // act + compiler.compileFunction( + createSharedFunctionStubWithCode(), + new FunctionCallStub() + .withArgumentCollection(expectedArgs), + ); + // assert + const actualArgs = expressionsCompilerStub.callHistory.map((call) => call.args[1]); + expect(actualArgs.every((arg) => arg === expectedArgs)); + }); + describe('execute', () => { + it('creates compiled code with compiled `execute`', () => { + // arrange + const func = createSharedFunctionStubWithCode(); + const args = new FunctionCallArgumentCollectionStub(); + const expectedCode = 'expected-code'; + const expressionsCompilerStub = new ExpressionsCompilerStub() + .setup({ + givenCode: func.body.code.execute, + givenArgs: args, + result: expectedCode, + }); + const compiler = new InlineFunctionCallCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .build(); + // act + const compiledCodes = compiler + .compileFunction(func, new FunctionCallStub().withArgumentCollection(args)); + // assert + expect(compiledCodes).to.have.lengthOf(1); + const actualCode = compiledCodes[0].code; + expect(actualCode).to.equal(expectedCode); + }); + }); + describe('revert', () => { + it('compiles to `undefined` when given `undefined`', () => { + // arrange + const expected = undefined; + const revertCode = undefined; + const func = createSharedFunctionStubWithCode() + .withRevertCode(revertCode); + const compiler = new InlineFunctionCallCompilerBuilder() + .build(); + // act + const compiledCodes = compiler + .compileFunction(func, new FunctionCallStub()); + // assert + expect(compiledCodes).to.have.lengthOf(1); + const actualRevertCode = compiledCodes[0].revertCode; + expect(actualRevertCode).to.equal(expected); + }); + it('creates compiled revert code with compiled `revert`', () => { + // arrange + const revertCode = 'revert-code-input'; + const func = createSharedFunctionStubWithCode() + .withRevertCode(revertCode); + const args = new FunctionCallArgumentCollectionStub(); + const expectedRevertCode = 'expected-revert-code'; + const expressionsCompilerStub = new ExpressionsCompilerStub() + .setup({ + givenCode: revertCode, + givenArgs: args, + result: expectedRevertCode, + }); + const compiler = new InlineFunctionCallCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .build(); + // act + const compiledCodes = compiler + .compileFunction(func, new FunctionCallStub().withArgumentCollection(args)); + // assert + expect(compiledCodes).to.have.lengthOf(1); + const actualRevertCode = compiledCodes[0].revertCode; + expect(actualRevertCode).to.equal(expectedRevertCode); + }); + }); + }); +}); + +class InlineFunctionCallCompilerBuilder { + private expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub(); + + public build(): InlineFunctionCallCompiler { + return new InlineFunctionCallCompiler(this.expressionsCompiler); + } + + public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this { + this.expressionsCompiler = expressionsCompiler; + return this; + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.spec.ts new file mode 100644 index 00000000..720dc90e --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.spec.ts @@ -0,0 +1,207 @@ +import { describe, it, expect } from 'vitest'; +import type { FunctionCallsData, FunctionCallData } from '@/application/collections/'; +import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser'; +import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import type { + NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator, +} from '@/application/Parser/Common/TypeValidator'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { FunctionCallArgumentFactoryStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub'; + +describe('FunctionCallsParser', () => { + describe('parseFunctionCalls', () => { + describe('throws if single call is not an object or array', () => { + // arrange + const expectedError = 'called function(s) must be an object or array'; + const testScenarios: readonly { + readonly description: string; + readonly invalidData: FunctionCallsData; + }[] = [ + { + description: 'given a string', + invalidData: 'string' as unknown as FunctionCallsData, + }, + { + description: 'given a number', + invalidData: 33 as unknown as FunctionCallsData, + }, + { + description: 'given a boolean', + invalidData: false as unknown as FunctionCallsData, + }, + { + description: 'given null', + invalidData: null as unknown as FunctionCallsData, + }, + { + description: 'given undefined', + invalidData: undefined as unknown as FunctionCallsData, + }, + ]; + testScenarios.forEach(({ description, invalidData }) => { + it(description, () => { + const context = new TestContext() + .withData(invalidData); + // act + const act = () => context.parse(); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); + describe('given a single call', () => { + it('validates single call as object', () => { + // arrange + const data = new FunctionCallDataStub(); + const expectedAssertion: ObjectAssertion = { + value: data, + valueName: 'Function call', + allowedProperties: [ + 'function', 'parameters', + ], + }; + const validator = new TypeValidatorStub(); + const context = new TestContext() + .withData(data) + .withTypeValidator(validator); + // act + context.parse(); + // assert + validator.expectObjectAssertion(expectedAssertion); + }); + it('parses single call as expected', () => { + // arrange + const expectedFunctionName = 'functionName'; + const expectedParameterName = 'parameterName'; + const expectedArgumentValue = 'argumentValue'; + const data = new FunctionCallDataStub() + .withName(expectedFunctionName) + .withParameters({ [expectedParameterName]: expectedArgumentValue }); + // act + const actual = parseFunctionCalls(data); + // assert + expect(actual).to.have.lengthOf(1); + const call = actual[0]; + expect(call.functionName).to.equal(expectedFunctionName); + const { args } = call; + expect(args.getAllParameterNames()).to.have.lengthOf(1); + expect(args.hasArgument(expectedParameterName)).to.equal( + true, + `Does not include expected parameter: "${expectedParameterName}"\n` + + `But includes: "${args.getAllParameterNames()}"`, + ); + const argument = args.getArgument(expectedParameterName); + expect(argument.parameterName).to.equal(expectedParameterName); + expect(argument.argumentValue).to.equal(expectedArgumentValue); + }); + }); + describe('given a call sequence', () => { + describe('throws if call sequence has undefined function name', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing function name in function call'; + const data = [ + new FunctionCallDataStub().withName('function-name'), + new FunctionCallDataStub().withName(absentValue), + ]; + // act + const act = () => parseFunctionCalls(data); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + it('validates call sequence as non empty collection', () => { + // arrange + const data: FunctionCallsData = [new FunctionCallDataStub()]; + const expectedAssertion: NonEmptyCollectionAssertion = { + value: data, + valueName: 'Function call sequence', + }; + const validator = new TypeValidatorStub(); + const context = new TestContext() + .withData(data) + .withTypeValidator(validator); + // act + context.parse(); + // assert + validator.expectNonEmptyCollectionAssertion(expectedAssertion); + }); + it('validates a call in call sequence as object', () => { + // arrange + const expectedValidatedCallData = new FunctionCallDataStub(); + const data: FunctionCallsData = [expectedValidatedCallData]; + const expectedAssertion: ObjectAssertion = { + value: expectedValidatedCallData, + valueName: 'Function call', + allowedProperties: [ + 'function', 'parameters', + ], + }; + const validator = new TypeValidatorStub(); + const context = new TestContext() + .withData(data) + .withTypeValidator(validator); + // act + context.parse(); + // assert + validator.expectObjectAssertion(expectedAssertion); + }); + it('parses multiple calls as expected', () => { + // arrange + const getFunctionName = (index: number) => `functionName${index}`; + const getParameterName = (index: number) => `parameterName${index}`; + const getArgumentValue = (index: number) => `argumentValue${index}`; + const createCall = (index: number) => new FunctionCallDataStub() + .withName(getFunctionName(index)) + .withParameters({ [getParameterName(index)]: getArgumentValue(index) }); + const calls = [createCall(0), createCall(1), createCall(2), createCall(3)]; + // act + const actual = parseFunctionCalls(calls); + // assert + expect(actual).to.have.lengthOf(calls.length); + for (let i = 0; i < calls.length; i++) { + const call = actual[i]; + const expectedParameterName = getParameterName(i); + const expectedArgumentValue = getArgumentValue(i); + expect(call.functionName).to.equal(getFunctionName(i)); + expect(call.args.getAllParameterNames()).to.have.lengthOf(1); + expect(call.args.hasArgument(expectedParameterName)).to.equal(true); + const argument = call.args.getArgument(expectedParameterName); + expect(argument.parameterName).to.equal(expectedParameterName); + expect(argument.argumentValue).to.equal(expectedArgumentValue); + } + }); + }); + }); +}); + +class TestContext { + private typeValidator: TypeValidator = new TypeValidatorStub(); + + private createCallArgument + : FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory; + + private calls: FunctionCallsData = [new FunctionCallDataStub()]; + + public withTypeValidator(typeValidator: TypeValidator): this { + this.typeValidator = typeValidator; + return this; + } + + public withData(calls: FunctionCallsData): this { + this.calls = calls; + return this; + } + + public parse(): ReturnType { + return parseFunctionCalls( + this.calls, + { + typeValidator: this.typeValidator, + createCallArgument: this.createCallArgument, + }, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall.spec.ts new file mode 100644 index 00000000..9f93f881 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall.spec.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from 'vitest'; +import { ParsedFunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall'; +import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('ParsedFunctionCall', () => { + describe('ctor', () => { + describe('args', () => { + it('sets args as expected', () => { + // arrange + const expected = new FunctionCallArgumentCollectionStub() + .withArgument('testParameter', 'testValue'); + // act + const sut = new FunctionCallBuilder() + .withArgs(expected) + .build(); + // assert + expect(sut.args).to.deep.equal(expected); + }); + }); + describe('functionName', () => { + describe('throws when function name is missing', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing function name in function call'; + const functionName = absentValue; + // act + const act = () => new FunctionCallBuilder() + .withFunctionName(functionName) + .build(); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + it('sets function name as expected', () => { + // arrange + const expected = 'expectedFunctionName'; + // act + const sut = new FunctionCallBuilder() + .withFunctionName(expected) + .build(); + // assert + expect(sut.functionName).to.equal(expected); + }); + }); + }); +}); + +class FunctionCallBuilder { + private functionName = 'functionName'; + + private args: IReadOnlyFunctionCallArgumentCollection = new FunctionCallArgumentCollectionStub(); + + public withFunctionName(functionName: string) { + this.functionName = functionName; + return this; + } + + public withArgs(args: IReadOnlyFunctionCallArgumentCollection) { + this.args = args; + return this; + } + + public build() { + return new ParsedFunctionCall(this.functionName, this.args); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/ExpectFunctionBodyType.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/ExpectFunctionBodyType.ts new file mode 100644 index 00000000..52c62e6b --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/ExpectFunctionBodyType.ts @@ -0,0 +1,28 @@ +import type { + CallFunctionBody, CodeFunctionBody, SharedFunctionBody, +} from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction'; +import { FunctionBodyType } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; + +export function expectCodeFunctionBody( + body: SharedFunctionBody, +): asserts body is CodeFunctionBody { + expectBodyType(body, FunctionBodyType.Code); +} + +export function expectCallsFunctionBody( + body: SharedFunctionBody, +): asserts body is CallFunctionBody { + expectBodyType(body, FunctionBodyType.Calls); +} + +function expectBodyType(body: SharedFunctionBody, expectedType: FunctionBodyType) { + const actualType = body.type; + expectExists(actualType, 'Function has no body'); + expect(actualType).to.equal(expectedType, formatAssertionMessage([ + `Actual: ${FunctionBodyType[actualType]}`, + `Expected: ${FunctionBodyType[expectedType]}`, + `Body: ${JSON.stringify(body)}`, + ])); +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection.spec.ts new file mode 100644 index 00000000..f488064b --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection.spec.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest'; +import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection'; +import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub'; + +describe('FunctionParameterCollection', () => { + it('all returns added parameters as expected', () => { + // arrange + const expected = [ + new FunctionParameterStub().withName('1'), + new FunctionParameterStub().withName('2').withOptional(true), + new FunctionParameterStub().withName('3').withOptional(false), + ]; + const sut = new FunctionParameterCollection(); + for (const parameter of expected) { + sut.addParameter(parameter); + } + // act + const actual = sut.all; + // assert + expect(expected).to.deep.equal(actual); + }); + it('throws when function parameters have same names', () => { + // arrange + const parameterName = 'duplicate-parameter'; + const expectedError = `duplicate parameter name: "${parameterName}"`; + const sut = new FunctionParameterCollection(); + sut.addParameter(new FunctionParameterStub().withName(parameterName)); + // act + const act = () => sut.addParameter( + new FunctionParameterStub().withName(parameterName), + ); + // assert + expect(act).to.throw(expectedError); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory.spec.ts new file mode 100644 index 00000000..07351ba5 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection'; +import { createFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory'; +import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests'; + +describe('FunctionParameterCollectionFactory', () => { + describe('createFunctionParameterCollection', () => { + describe('it is a transient factory', () => { + itIsTransientFactory({ + getter: () => createFunctionParameterCollection(), + expectedType: FunctionParameterCollection, + }); + }); + it('returns an empty collection', () => { + // arrange + const expectedInitialParametersCount = 0; + // act + const collection = createFunctionParameterCollection(); + // assert + expect(collection.all).to.have.lengthOf(expectedInitialParametersCount); + }); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.spec.ts new file mode 100644 index 00000000..fde3ee51 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.spec.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest'; +import type { ParameterDefinitionData } from '@/application/collections/'; +import type { ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; +import { createParameterNameValidatorStub } from '@tests/unit/shared/Stubs/ParameterNameValidatorStub'; +import { parseFunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser'; +import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub'; + +describe('FunctionParameterParser', () => { + describe('parseFunctionParameter', () => { + describe('name', () => { + it('assigns correctly', () => { + // arrange + const expectedName = 'expected-function-name'; + const data = new ParameterDefinitionDataStub() + .withName(expectedName); + // act + const actual = new TestContext() + .withData(data) + .parse(); + // expect + const actualName = actual.name; + expect(actualName).to.equal(expectedName); + }); + it('validates correctly', () => { + // arrange + const expectedName = 'expected-function-name'; + const { validator, validatedNames } = createParameterNameValidatorStub(); + const data = new ParameterDefinitionDataStub() + .withName(expectedName); + // act + new TestContext() + .withData(data) + .withValidator(validator) + .parse(); + // expect + expect(validatedNames).to.have.lengthOf(1); + expect(validatedNames).to.contain(expectedName); + }); + }); + describe('isOptional', () => { + describe('assigns correctly', () => { + // arrange + const expectedValues = [true, false]; + for (const expected of expectedValues) { + it(expected.toString(), () => { + const data = new ParameterDefinitionDataStub() + .withOptionality(expected); + // act + const actual = new TestContext() + .withData(data) + .parse(); + // expect + expect(actual.isOptional).to.equal(expected); + }); + } + }); + }); + }); +}); + +class TestContext { + private data: ParameterDefinitionData = new ParameterDefinitionDataStub() + .withName(`[${TestContext.name}]function-name`); + + private validator: ParameterNameValidator = createParameterNameValidatorStub().validator; + + public withData(data: ParameterDefinitionData) { + this.data = data; + return this; + } + + public withValidator(parameterNameValidator: ParameterNameValidator): this { + this.validator = parameterNameValidator; + return this; + } + + public parse() { + return parseFunctionParameter( + this.data, + this.validator, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunction.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunction.spec.ts new file mode 100644 index 00000000..865d58e6 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunction.spec.ts @@ -0,0 +1,240 @@ +import { describe, it, expect } from 'vitest'; +import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection'; +import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub'; +import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunction'; +import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { type CallFunctionBody, FunctionBodyType, type ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction'; +import { + getAbsentStringTestCases, itEachAbsentCollectionValue, + itEachAbsentStringValue, +} from '@tests/unit/shared/TestCases/AbsentTests'; +import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType'; + +describe('SharedFunction', () => { + describe('SharedFunction', () => { + describe('name', () => { + runForEachFactoryMethod((build) => { + it('sets as expected', () => { + // arrange + const expected = 'expected-function-name'; + const builder = new SharedFunctionBuilder() + .withName(expected); + // act + const sut = build(builder); + // assert + expect(sut.name).equal(expected); + }); + describe('throws when absent', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing function name'; + const builder = new SharedFunctionBuilder() + .withName(absentValue); + // act + const act = () => build(builder); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + }); + }); + describe('parameters', () => { + runForEachFactoryMethod((build) => { + it('sets as expected', () => { + // arrange + const expected = new FunctionParameterCollectionStub() + .withParameterName('test-parameter'); + const builder = new SharedFunctionBuilder() + .withParameters(expected); + // act + const sut = build(builder); + // assert + expect(sut.parameters).equal(expected); + }); + }); + }); + }); + describe('createFunctionWithInlineCode', () => { + describe('code', () => { + it('sets as expected', () => { + // arrange + const expected = 'expected-code'; + // act + const sut = new SharedFunctionBuilder() + .withCode(expected) + .createFunctionWithInlineCode(); + // assert + expectCodeFunctionBody(sut.body); + expect(sut.body.code.execute).equal(expected); + }); + describe('throws if absent', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const functionName = 'expected-function-name'; + const expectedError = `undefined code in function "${functionName}"`; + const invalidValue = absentValue; + // act + const act = () => new SharedFunctionBuilder() + .withName(functionName) + .withCode(invalidValue) + .createFunctionWithInlineCode(); + // assert + expect(act).to.throw(expectedError); + }, { excludeUndefined: true, excludeNull: true }); + }); + }); + describe('revertCode', () => { + it('sets as expected', () => { + // arrange + const revertCodeTestValues: readonly (string | undefined)[] = [ + 'expected-revert-code', + ...getAbsentStringTestCases({ + excludeNull: true, + }).map((testCase) => testCase.absentValue), + ]; + for (const revertCode of revertCodeTestValues) { + // act + const sut = new SharedFunctionBuilder() + .withRevertCode(revertCode) + .createFunctionWithInlineCode(); + // assert + expectCodeFunctionBody(sut.body); + expect(sut.body.code.revert).equal(revertCode); + } + }); + }); + it('sets type as expected', () => { + // arrange + const expectedType = FunctionBodyType.Code; + // act + const sut = new SharedFunctionBuilder() + .createFunctionWithInlineCode(); + // assert + expect(sut.body.type).equal(expectedType); + }); + it('calls are undefined', () => { + // arrange + const expectedCalls = undefined; + // act + const sut = new SharedFunctionBuilder() + .createFunctionWithInlineCode(); + // assert + expect((sut.body as CallFunctionBody).calls).equal(expectedCalls); + }); + }); + describe('createCallerFunction', () => { + describe('rootCallSequence', () => { + it('sets as expected', () => { + // arrange + const expected = [ + new FunctionCallStub().withFunctionName('firstFunction'), + new FunctionCallStub().withFunctionName('secondFunction'), + ]; + // act + const sut = new SharedFunctionBuilder() + .withRootCallSequence(expected) + .createCallerFunction(); + // assert + expectCallsFunctionBody(sut.body); + expect(sut.body.calls).equal(expected); + }); + describe('throws if missing', () => { + itEachAbsentCollectionValue((absentValue) => { + // arrange + const functionName = 'invalidFunction'; + const rootCallSequence = absentValue; + const expectedError = `missing call sequence in function "${functionName}"`; + // act + const act = () => new SharedFunctionBuilder() + .withName(functionName) + .withRootCallSequence(rootCallSequence) + .createCallerFunction(); + // assert + expect(act).to.throw(expectedError); + }, { excludeUndefined: true, excludeNull: true }); + }); + }); + it('sets type as expected', () => { + // arrange + const expectedType = FunctionBodyType.Calls; + // act + const sut = new SharedFunctionBuilder() + .createCallerFunction(); + // assert + expect(sut.body.type).equal(expectedType); + }); + }); +}); + +function runForEachFactoryMethod( + act: (action: (sut: SharedFunctionBuilder) => ISharedFunction) => void, +): void { + describe('createCallerFunction', () => { + const action = (builder: SharedFunctionBuilder) => builder.createCallerFunction(); + act(action); + }); + describe('createFunctionWithInlineCode', () => { + const action = (builder: SharedFunctionBuilder) => builder.createFunctionWithInlineCode(); + act(action); + }); +} + +/* + Using an abstraction here allows for easy refactorings in + parameters or moving between functional and object-oriented + solutions without refactorings all tests. +*/ +class SharedFunctionBuilder { + private name = 'name'; + + private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub(); + + private callSequence: readonly FunctionCall[] = [new FunctionCallStub()]; + + private code = `[${SharedFunctionBuilder.name}] code`; + + private revertCode: string | undefined = `[${SharedFunctionBuilder.name}] revert-code`; + + public createCallerFunction(): ISharedFunction { + return createCallerFunction( + this.name, + this.parameters, + this.callSequence, + ); + } + + public createFunctionWithInlineCode(): ISharedFunction { + return createFunctionWithInlineCode( + this.name, + this.parameters, + this.code, + this.revertCode, + ); + } + + public withName(name: string) { + this.name = name; + return this; + } + + public withParameters(parameters: IReadOnlyFunctionParameterCollection) { + this.parameters = parameters; + return this; + } + + public withCode(code: string) { + this.code = code; + return this; + } + + public withRevertCode(revertCode: string | undefined) { + this.revertCode = revertCode; + return this; + } + + public withRootCallSequence(callSequence: readonly FunctionCall[]) { + this.callSequence = callSequence; + return this; + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionCollection.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionCollection.spec.ts new file mode 100644 index 00000000..1bcb5665 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionCollection.spec.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest'; +import { SharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionCollection'; +import { createSharedFunctionStubWithCode, createSharedFunctionStubWithCalls } from '@tests/unit/shared/Stubs/SharedFunctionStub'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('SharedFunctionCollection', () => { + describe('addFunction', () => { + it('throws if function with same name already exists', () => { + // arrange + const functionName = 'duplicate-function'; + const expectedError = `function with name ${functionName} already exists`; + const func = createSharedFunctionStubWithCode() + .withName('duplicate-function'); + const sut = new SharedFunctionCollection(); + sut.addFunction(func); + // act + const act = () => sut.addFunction(func); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('getFunctionByName', () => { + describe('throws if name is absent', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing function name'; + const sut = new SharedFunctionCollection(); + // act + const act = () => sut.getFunctionByName(absentValue); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + it('throws if function does not exist', () => { + // arrange + const name = 'unique-name'; + const expectedError = `Called function is not defined: "${name}"`; + const func = createSharedFunctionStubWithCode() + .withName('unexpected-name'); + const sut = new SharedFunctionCollection(); + sut.addFunction(func); + // act + const act = () => sut.getFunctionByName(name); + // assert + expect(act).to.throw(expectedError); + }); + describe('returns existing function', () => { + it('when function with inline code is added', () => { + // arrange + const expected = createSharedFunctionStubWithCode() + .withName('expected-function-name'); + const sut = new SharedFunctionCollection(); + // act + sut.addFunction(expected); + const actual = sut.getFunctionByName(expected.name); + // assert + expect(actual).to.equal(expected); + }); + it('when calling function is added', () => { + // arrange + const callee = createSharedFunctionStubWithCode() + .withName('calleeFunction'); + const caller = createSharedFunctionStubWithCalls() + .withName('callerFunction') + .withCalls(new FunctionCallStub().withFunctionName(callee.name)); + const sut = new SharedFunctionCollection(); + // act + sut.addFunction(callee); + sut.addFunction(caller); + const actual = sut.getFunctionByName(caller.name); + // assert + expect(actual).to.equal(caller); + }); + }); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.spec.ts new file mode 100644 index 00000000..adac3332 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.spec.ts @@ -0,0 +1,513 @@ +import { describe, it, expect } from 'vitest'; +import type { + FunctionData, CodeInstruction, + ParameterDefinitionData, FunctionCallsData, +} from '@/application/collections/'; +import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction'; +import { parseSharedFunctions } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser'; +import { createFunctionDataWithCall, createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub'; +import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub'; +import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub'; +import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax'; +import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; +import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub'; +import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; +import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines'; +import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines'; +import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub'; +import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; +import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory'; +import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub'; +import type { FunctionCallsParser } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser'; +import { createFunctionCallsParserStub } from '@tests/unit/shared/Stubs/FunctionCallsParserStub'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection'; +import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; +import type { FunctionParameterParser } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser'; +import { createFunctionParameterParserStub } from '@tests/unit/shared/Stubs/FunctionParameterParserStub'; +import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType'; + +describe('SharedFunctionsParser', () => { + describe('parseSharedFunctions', () => { + describe('validates functions', () => { + it('throws when no name is provided', () => { + // arrange + const invalidFunctions = [ + createFunctionDataWithCode() + .withCode('test function 1') + .withName(' '), // Whitespace, + createFunctionDataWithCode() + .withCode('test function 2') + .withName(undefined as unknown as string), // Undefined + createFunctionDataWithCode() + .withCode('test function 3') + .withName(''), // Empty + ]; + const expectedError = `Some function(s) have no names:\n${invalidFunctions.map((f) => JSON.stringify(f)).join('\n')}`; + // act + const act = () => new TestContext() + .withFunctions(invalidFunctions) + .parseFunctions(); + // assert + expect(act).to.throw(expectedError); + }); + it('throws when functions have duplicate names', () => { + // arrange + const name = 'same-func-name'; + const expectedError = `duplicate function name: "${name}"`; + const functions = [ + createFunctionDataWithCode().withName(name), + createFunctionDataWithCode().withName(name), + ]; + // act + const act = () => new TestContext() + .withFunctions(functions) + .parseFunctions(); + // assert + expect(act).to.throw(expectedError); + }); + describe('throws when functions have duplicate code', () => { + it('throws on code duplication', () => { + // arrange + const code = 'duplicate-code'; + const expectedError = `duplicate "code" in functions: "${code}"`; + const functions = [ + createFunctionDataWithoutCallOrCode().withName('func-1').withCode(code), + createFunctionDataWithoutCallOrCode().withName('func-2').withCode(code), + ]; + // act + const act = () => new TestContext() + .withFunctions(functions) + .parseFunctions(); + // assert + expect(act).to.throw(expectedError); + }); + it('throws on revert code duplication', () => { + // arrange + const revertCode = 'duplicate-revert-code'; + const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`; + const functions = [ + createFunctionDataWithoutCallOrCode() + .withName('func-1').withCode('code-1').withRevertCode(revertCode), + createFunctionDataWithoutCallOrCode() + .withName('func-2').withCode('code-2').withRevertCode(revertCode), + ]; + // act + const act = () => new TestContext() + .withFunctions(functions) + .parseFunctions(); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('throws when both or neither code and call are defined', () => { + it('throws when both code and call are defined', () => { + // arrange + const functionName = 'invalid-function'; + const expectedError = `both "code" and "call" are defined in "${functionName}"`; + const invalidFunction = createFunctionDataWithoutCallOrCode() + .withName(functionName) + .withCode('code') + .withMockCall(); + // act + const act = () => new TestContext() + .withFunctions([invalidFunction]) + .parseFunctions(); + // assert + expect(act).to.throw(expectedError); + }); + it('throws when neither code nor call is defined', () => { + // arrange + const functionName = 'invalid-function'; + const expectedError = `neither "code" or "call" is defined in "${functionName}"`; + const invalidFunction = createFunctionDataWithoutCallOrCode() + .withName(functionName); + // act + const act = () => new TestContext() + .withFunctions([invalidFunction]) + .parseFunctions(); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('throws when parameter types are invalid', () => { + const testScenarios: readonly { + readonly description: string; + readonly invalidType: unknown; + }[] = [ + { + description: 'parameter is not an array', + invalidType: 5, + }, + { + description: 'parameter array contains non-objects', + invalidType: ['a', { a: 'b' }], + }, + ]; + for (const testCase of testScenarios) { + it(testCase.description, () => { + // arrange + const func = createFunctionDataWithCode() + .withParametersObject(testCase.invalidType as never); + const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`; + // act + const act = () => new TestContext() + .withFunctions([func]) + .parseFunctions(); + // assert + expect(act).to.throw(expectedError); + }); + } + }); + it('validates function code as expected when code is defined', () => { + // arrange + const expectedRules = [NoEmptyLines, NoDuplicatedLines]; + const functionData = createFunctionDataWithCode() + .withCode('expected code to be validated') + .withRevertCode('expected revert code to be validated'); + const validator = new CodeValidatorStub(); + // act + new TestContext() + .withFunctions([functionData]) + .withValidator(validator) + .parseFunctions(); + // assert + validator.assertHistory({ + validatedCodes: [functionData.code, functionData.revertCode], + rules: expectedRules, + }); + }); + describe('parameter creation', () => { + describe('rethrows including function name when creating parameter throws', () => { + // arrange + const invalidParameterName = 'invalid-function-parameter-name'; + const functionName = 'functionName'; + const expectedErrorMessage = `Failed to create parameter: ${invalidParameterName} for function "${functionName}"`; + const expectedInnerError = new Error('injected error'); + const parser: FunctionParameterParser = () => { + throw expectedInnerError; + }; + const functionData = createFunctionDataWithCode() + .withName(functionName) + .withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName)); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + new TestContext() + .withFunctions([functionData]) + .withFunctionParameterParser(parser) + .withErrorWrapper(wrapError) + .parseFunctions(); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + }); + }); + describe('handles empty function data', () => { + itEachAbsentCollectionValue((absentValue) => { + // act + const actual = new TestContext() + .withFunctions(absentValue) + .parseFunctions(); + // assert + expect(actual).to.not.equal(undefined); + }, { excludeUndefined: true, excludeNull: true }); + }); + describe('function with inline code', () => { + it('parses single function with code as expected', () => { + // arrange + const name = 'function-name'; + const expected = createFunctionDataWithoutCallOrCode() + .withName(name) + .withCode('expected-code') + .withRevertCode('expected-revert-code') + .withParameters( + new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true), + new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false), + ); + // act + const collection = new TestContext() + .withFunctions([expected]) + .parseFunctions(); + // expect + const actual = collection.getFunctionByName(name); + expectEqualName(expected, actual); + expectEqualParameters(expected.parameters, actual.parameters); + expectEqualFunctionWithInlineCode(expected, actual); + }); + }); + describe('function with calls', () => { + describe('parses single function correctly', () => { + it('parses name correctly', () => { + // arrange + const expectedName = 'expected-function-name'; + const data = createFunctionDataWithCode() + .withName(expectedName); + // act + const collection = new TestContext() + .withFunctions([data]) + .parseFunctions(); + // expect + const actual = collection.getFunctionByName(expectedName); + expect(actual.name).to.equal(expectedName); + expectEqualName(data, actual); + }); + it('parses parameters correctly', () => { + // arrange + const functionCallsParserStub = createFunctionCallsParserStub(); + const expectedParameters: readonly ParameterDefinitionData[] = [ + new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true), + new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false), + ]; + const data = createFunctionDataWithCode() + .withParameters(...expectedParameters); + // act + const collection = new TestContext() + .withFunctions([data]) + .withFunctionCallsParser(functionCallsParserStub.parser) + .parseFunctions(); + // expect + const actual = collection.getFunctionByName(data.name); + expectEqualParameters(expectedParameters, actual.parameters); + }); + it('parses call correctly', () => { + // arrange + const functionCallsParserStub = createFunctionCallsParserStub(); + const inputCallData = new FunctionCallDataStub() + .withName('function-input-call'); + const data = createFunctionDataWithoutCallOrCode() + .withCall(inputCallData); + const expectedCall = new FunctionCallStub() + .withFunctionName('function-expected-call'); + functionCallsParserStub.setup(inputCallData, [expectedCall]); + // act + const collection = new TestContext() + .withFunctions([data]) + .withFunctionCallsParser(functionCallsParserStub.parser) + .parseFunctions(); + // expect + const actualFunction = collection.getFunctionByName(data.name); + expectEqualFunctionWithCalls([expectedCall], actualFunction); + }); + }); + describe('parses multiple functions correctly', () => { + it('parses names correctly', () => { + // arrange + const expectedNames: readonly string[] = [ + 'expected-function-name-1', + 'expected-function-name-2', + 'expected-function-name-3', + ]; + const data: readonly FunctionData[] = expectedNames.map( + (functionName) => createFunctionDataWithCall() + .withName(functionName), + ); + // act + const collection = new TestContext() + .withFunctions(data) + .parseFunctions(); + // expect + expectedNames.forEach((name, index) => { + const compiledFunction = collection.getFunctionByName(name); + expectEqualName(data[index], compiledFunction); + }); + }); + it('parses parameters correctly', () => { + // arrange + const testData: readonly { + readonly functionName: string; + readonly inputParameterData: readonly ParameterDefinitionData[]; + }[] = [ + { + functionName: 'func1', + inputParameterData: [ + new ParameterDefinitionDataStub().withName('func1-first-parameter'), + new ParameterDefinitionDataStub().withName('func1-second-parameter'), + ], + }, + { + functionName: 'func2', + inputParameterData: [ + new ParameterDefinitionDataStub().withName('func2-optional-parameter').withOptionality(true), + new ParameterDefinitionDataStub().withName('func2-required-parameter').withOptionality(false), + ], + }, + ]; + const data: readonly FunctionData[] = testData.map( + (d) => createFunctionDataWithCall() + .withName(d.functionName) + .withParameters(...d.inputParameterData), + ); + // act + const collection = new TestContext() + .withFunctions(data) + .parseFunctions(); + // expect + testData.forEach(({ functionName, inputParameterData }) => { + const actualFunction = collection.getFunctionByName(functionName); + expectEqualParameters(inputParameterData, actualFunction.parameters); + }); + }); + it('parses call correctly', () => { + // arrange + const functionCallsParserStub = createFunctionCallsParserStub(); + const callData: readonly { + readonly functionName: string; + readonly inputData: FunctionCallsData, + readonly expectedCalls: ReturnType, + }[] = [ + { + functionName: 'function-1', + inputData: new FunctionCallDataStub().withName('function-1-input-function-call'), + expectedCalls: [ + new FunctionCallStub().withFunctionName('function-1-compiled-function-call'), + ], + }, + { + functionName: 'function-2', + inputData: [ + new FunctionCallDataStub().withName('function-2-input-function-call-1'), + new FunctionCallDataStub().withName('function-2-input-function-call-2'), + ], + expectedCalls: [ + new FunctionCallStub().withFunctionName('function-2-compiled-function-call-1'), + new FunctionCallStub().withFunctionName('function-2-compiled-function-call-2'), + ], + }, + ]; + const data: readonly FunctionData[] = callData.map( + ({ functionName, inputData }) => createFunctionDataWithoutCallOrCode() + .withName(functionName) + .withCall(inputData), + ); + callData.forEach(({ + inputData, + expectedCalls, + }) => functionCallsParserStub.setup(inputData, expectedCalls)); + // act + const collection = new TestContext() + .withFunctions(data) + .withFunctionCallsParser(functionCallsParserStub.parser) + .parseFunctions(); + // expect + callData.forEach(({ functionName, expectedCalls }) => { + const actualFunction = collection.getFunctionByName(functionName); + expectEqualFunctionWithCalls(expectedCalls, actualFunction); + }); + }); + }); + }); + }); +}); + +class TestContext { + private syntax: ILanguageSyntax = new LanguageSyntaxStub(); + + private codeValidator: ICodeValidator = new CodeValidatorStub(); + + private functions: readonly FunctionData[] = [createFunctionDataWithCode()]; + + private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub; + + private functionCallsParser: FunctionCallsParser = createFunctionCallsParserStub().parser; + + private functionParameterParser: FunctionParameterParser = createFunctionParameterParserStub; + + private parameterCollectionFactory + : FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub(); + + public withSyntax(syntax: ILanguageSyntax): this { + this.syntax = syntax; + return this; + } + + public withValidator(codeValidator: ICodeValidator): this { + this.codeValidator = codeValidator; + return this; + } + + public withFunctionCallsParser(functionCallsParser: FunctionCallsParser): this { + this.functionCallsParser = functionCallsParser; + return this; + } + + public withFunctions(functions: readonly FunctionData[]): this { + this.functions = functions; + return this; + } + + public withErrorWrapper(wrapError: ErrorWithContextWrapper): this { + this.wrapError = wrapError; + return this; + } + + public withFunctionParameterParser(functionParameterParser: FunctionParameterParser): this { + this.functionParameterParser = functionParameterParser; + return this; + } + + public withParameterCollectionFactory( + parameterCollectionFactory: FunctionParameterCollectionFactory, + ): this { + this.parameterCollectionFactory = parameterCollectionFactory; + return this; + } + + public parseFunctions(): ReturnType { + return parseSharedFunctions( + this.functions, + this.syntax, + { + codeValidator: this.codeValidator, + wrapError: this.wrapError, + parseParameter: this.functionParameterParser, + createParameterCollection: this.parameterCollectionFactory, + parseFunctionCalls: this.functionCallsParser, + }, + ); + } +} + +function expectEqualName(expected: FunctionData, actual: ISharedFunction): void { + expect(actual.name).to.equal(expected.name); +} + +function expectEqualParameters( + expected: readonly ParameterDefinitionData[] | undefined, + actual: IReadOnlyFunctionParameterCollection, +): void { + const actualSimplifiedParameters = actual.all.map((parameter) => ({ + name: parameter.name, + optional: parameter.isOptional, + })); + const expectedSimplifiedParameters = expected?.map((parameter) => ({ + name: parameter.name, + optional: parameter.optional || false, + })) || []; + expect(expectedSimplifiedParameters).to.deep.equal(actualSimplifiedParameters, 'Unequal parameters'); +} + +function expectEqualFunctionWithInlineCode( + expected: CodeInstruction, + actual: ISharedFunction, +): void { + expectCodeFunctionBody(actual.body); + expect(actual.body.code, `function "${actual.name}" has no code`); + expect(actual.body.code.execute).to.equal(expected.code); + expect(actual.body.code.revert).to.equal(expected.revertCode); +} + +function expectEqualFunctionWithCalls( + expectedCalls: readonly FunctionCall[], + actualFunction: ISharedFunction, +): void { + expectCallsFunctionBody(actualFunction.body); + const actualCalls = actualFunction.body.calls; + expect(actualCalls.length).to.equal(expectedCalls.length); + expect(actualCalls).to.have.members(expectedCalls); +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameValidator.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameValidator.spec.ts new file mode 100644 index 00000000..7a1bcbe2 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameValidator.spec.ts @@ -0,0 +1,24 @@ +import { describe, it } from 'vitest'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import { validateParameterName } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; +import type { NonEmptyStringAssertion } from '@/application/Parser/Common/TypeValidator'; + +describe('ParameterNameValidator', () => { + it('asserts correctly', () => { + // arrange + const parameterName = 'expected-parameter-name'; + const validator = new TypeValidatorStub(); + const expectedAssertion: NonEmptyStringAssertion = { + value: parameterName, + valueName: 'parameter name', + rule: { + expectedMatch: /^[0-9a-zA-Z]+$/, + errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`, + }, + }; + // act + validateParameterName(parameterName, validator); + // assert + validator.assertNonEmptyString(expectedAssertion); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompiler.spec.ts new file mode 100644 index 00000000..5e985a85 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompiler.spec.ts @@ -0,0 +1,332 @@ +import { describe, it, expect } from 'vitest'; +import type { FunctionData } from '@/application/collections/'; +import { ScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompiler'; +import type { FunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler'; +import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; +import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub'; +import { FunctionCallCompilerStub } from '@tests/unit/shared/Stubs/FunctionCallCompilerStub'; +import { createSharedFunctionsParserStub } from '@tests/unit/shared/Stubs/SharedFunctionsParserStub'; +import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub'; +import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser'; +import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub'; +import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax'; +import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; +import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub'; +import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines'; +import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub'; +import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub'; +import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub'; +import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub'; +import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory'; +import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub'; +import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; +import type { SharedFunctionsParser } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser'; + +describe('ScriptCompiler', () => { + describe('canCompile', () => { + it('returns true if "call" is defined', () => { + // arrange + const sut = new ScriptCompilerBuilder() + .withEmptyFunctions() + .build(); + const script = createScriptDataWithCall(); + // act + const actual = sut.canCompile(script); + // assert + expect(actual).to.equal(true); + }); + it('returns false if "call" is undefined', () => { + // arrange + const sut = new ScriptCompilerBuilder() + .withEmptyFunctions() + .build(); + const script = createScriptDataWithCode(); + // act + const actual = sut.canCompile(script); + // assert + expect(actual).to.equal(false); + }); + }); + describe('compile', () => { + it('throws if script does not have body', () => { + // arrange + const expectedError = 'Script does include any calls.'; + const scriptData = createScriptDataWithCode(); + const sut = new ScriptCompilerBuilder() + .withSomeFunctions() + .build(); + // act + const act = () => sut.compile(scriptData); + // assert + expect(act).to.throw(expectedError); + }); + describe('code construction', () => { + it('returns code from the factory', () => { + // arrange + const expectedCode = new ScriptCodeStub(); + const scriptCodeFactory = () => expectedCode; + const sut = new ScriptCompilerBuilder() + .withSomeFunctions() + .withScriptCodeFactory(scriptCodeFactory) + .build(); + // act + const actualCode = sut.compile(createScriptDataWithCall()); + // assert + expect(actualCode).to.equal(expectedCode); + }); + it('creates code correctly', () => { + // arrange + const expectedCode = 'expected-code'; + const expectedRevertCode = 'expected-revert-code'; + let actualCode: string | undefined; + let actualRevertCode: string | undefined; + const scriptCodeFactory = (code: string, revertCode: string) => { + actualCode = code; + actualRevertCode = revertCode; + return new ScriptCodeStub(); + }; + const call = new FunctionCallDataStub(); + const script = createScriptDataWithCall(call); + const functions = [createFunctionDataWithCode().withName('existing-func')]; + const compiledFunctions = new SharedFunctionCollectionStub(); + const functionParserMock = createSharedFunctionsParserStub(); + functionParserMock.setup(functions, compiledFunctions); + const callCompilerMock = new FunctionCallCompilerStub(); + callCompilerMock.setup( + parseFunctionCalls(call), + compiledFunctions, + new CompiledCodeStub() + .withCode(expectedCode) + .withRevertCode(expectedRevertCode), + ); + const sut = new ScriptCompilerBuilder() + .withFunctions(...functions) + .withSharedFunctionsParser(functionParserMock.parser) + .withFunctionCallCompiler(callCompilerMock) + .withScriptCodeFactory(scriptCodeFactory) + .build(); + // act + sut.compile(script); + // assert + expect(actualCode).to.equal(expectedCode); + expect(actualRevertCode).to.equal(expectedRevertCode); + }); + }); + + describe('parses functions as expected', () => { + it('parses functions with expected syntax', () => { + // arrange + const expected: ILanguageSyntax = new LanguageSyntaxStub(); + const functionParserMock = createSharedFunctionsParserStub(); + const sut = new ScriptCompilerBuilder() + .withSomeFunctions() + .withSyntax(expected) + .withSharedFunctionsParser(functionParserMock.parser) + .build(); + const scriptData = createScriptDataWithCall(); + // act + sut.compile(scriptData); + // assert + const parserCalls = functionParserMock.callHistory; + expect(parserCalls.length).to.equal(1); + expect(parserCalls[0].syntax).to.equal(expected); + }); + it('parses given functions', () => { + // arrange + const expectedFunctions = [createFunctionDataWithCode().withName('existing-func')]; + const functionParserMock = createSharedFunctionsParserStub(); + const sut = new ScriptCompilerBuilder() + .withFunctions(...expectedFunctions) + .withSharedFunctionsParser(functionParserMock.parser) + .build(); + const scriptData = createScriptDataWithCall(); + // act + sut.compile(scriptData); + // assert + const parserCalls = functionParserMock.callHistory; + expect(parserCalls.length).to.equal(1); + expect(parserCalls[0].functions).to.deep.equal(expectedFunctions); + }); + }); + describe('rethrows error with script name', () => { + // arrange + const scriptName = 'scriptName'; + const expectedErrorMessage = `Failed to compile script: ${scriptName}`; + const expectedInnerError = new Error(); + const callCompiler: FunctionCallCompiler = { + compileFunctionCalls: () => { throw expectedInnerError; }, + }; + const scriptData = createScriptDataWithCall() + .withName(scriptName); + const builder = new ScriptCompilerBuilder() + .withSomeFunctions() + .withFunctionCallCompiler(callCompiler); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + builder + .withErrorWrapper(wrapError) + .build() + .compile(scriptData); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + describe('rethrows error from script code factory with script name', () => { + // arrange + const scriptName = 'scriptName'; + const expectedErrorMessage = `Failed to compile script: ${scriptName}`; + const expectedInnerError = new Error(); + const scriptCodeFactory: ScriptCodeFactory = () => { + throw expectedInnerError; + }; + const scriptData = createScriptDataWithCall() + .withName(scriptName); + const builder = new ScriptCompilerBuilder() + .withSomeFunctions() + .withScriptCodeFactory(scriptCodeFactory); + itThrowsContextualError({ + // act + throwingAction: (wrapError) => { + builder + .withErrorWrapper(wrapError) + .build() + .compile(scriptData); + }, + // assert + expectedWrappedError: expectedInnerError, + expectedContextMessage: expectedErrorMessage, + }); + }); + it('validates compiled code as expected', () => { + // arrange + const expectedRules = [ + NoEmptyLines, + // Allow duplicated lines to enable calling same function multiple times + ]; + const expectedExecuteCode = 'execute code to be validated'; + const expectedRevertCode = 'revert code to be validated'; + const scriptData = createScriptDataWithCall(); + const validator = new CodeValidatorStub(); + const sut = new ScriptCompilerBuilder() + .withSomeFunctions() + .withCodeValidator(validator) + .withFunctionCallCompiler( + new FunctionCallCompilerStub() + .withDefaultCompiledCode( + new CompiledCodeStub() + .withCode(expectedExecuteCode) + .withRevertCode(expectedRevertCode), + ), + ) + .build(); + // act + sut.compile(scriptData); + // assert + validator.assertHistory({ + validatedCodes: [expectedExecuteCode, expectedRevertCode], + rules: expectedRules, + }); + }); + }); +}); + +class ScriptCompilerBuilder { + private static createFunctions(...names: string[]): FunctionData[] { + return names.map((functionName) => { + return createFunctionDataWithCode().withName(functionName); + }); + } + + private functions: FunctionData[] | undefined; + + private syntax: ILanguageSyntax = new LanguageSyntaxStub(); + + private sharedFunctionsParser: SharedFunctionsParser = createSharedFunctionsParserStub().parser; + + private callCompiler: FunctionCallCompiler = new FunctionCallCompilerStub(); + + private codeValidator: ICodeValidator = new CodeValidatorStub(); + + private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub; + + private scriptCodeFactory: ScriptCodeFactory = createScriptCodeFactoryStub({ + defaultCodePrefix: ScriptCompilerBuilder.name, + }); + + public withFunctions(...functions: FunctionData[]): this { + this.functions = functions; + return this; + } + + public withSomeFunctions(): this { + this.functions = ScriptCompilerBuilder.createFunctions('test-function'); + return this; + } + + public withFunctionNames(...functionNames: string[]): this { + this.functions = ScriptCompilerBuilder.createFunctions(...functionNames); + return this; + } + + public withEmptyFunctions(): this { + this.functions = []; + return this; + } + + public withSyntax(syntax: ILanguageSyntax): this { + this.syntax = syntax; + return this; + } + + public withSharedFunctionsParser( + sharedFunctionsParser: SharedFunctionsParser, + ): this { + this.sharedFunctionsParser = sharedFunctionsParser; + return this; + } + + public withCodeValidator( + codeValidator: ICodeValidator, + ): this { + this.codeValidator = codeValidator; + return this; + } + + public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): this { + this.callCompiler = callCompiler; + return this; + } + + public withErrorWrapper(wrapError: ErrorWithContextWrapper): this { + this.wrapError = wrapError; + return this; + } + + public withScriptCodeFactory(scriptCodeFactory: ScriptCodeFactory): this { + this.scriptCodeFactory = scriptCodeFactory; + return this; + } + + public build(): ScriptCompiler { + if (!this.functions) { + throw new Error('Function behavior not defined'); + } + return new ScriptCompiler( + { + functions: this.functions, + syntax: this.syntax, + }, + { + sharedFunctionsParser: this.sharedFunctionsParser, + callCompiler: this.callCompiler, + codeValidator: this.codeValidator, + wrapError: this.wrapError, + scriptCodeFactory: this.scriptCodeFactory, + }, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/ScriptParser.spec.ts b/tests/unit/application/Parser/Executable/Script/ScriptParser.spec.ts new file mode 100644 index 00000000..74b034c7 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/ScriptParser.spec.ts @@ -0,0 +1,546 @@ +import { describe, it, expect } from 'vitest'; +import type { ScriptData, CallScriptData, CodeScriptData } from '@/application/collections/'; +import { parseScript } from '@/application/Parser/Executable/Script/ScriptParser'; +import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser'; +import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub'; +import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub'; +import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub'; +import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub'; +import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; +import type { EnumParser } from '@/application/Common/Enum'; +import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines'; +import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines'; +import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub'; +import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; +import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub'; +import type { ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator'; +import { ExecutableValidatorStub, createExecutableValidatorFactoryStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub'; +import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType'; +import type { ScriptErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext'; +import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory'; +import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { createScriptFactorySpy } from '@tests/unit/shared/Stubs/ScriptFactoryStub'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; +import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub'; +import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities'; +import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; +import type { ScriptFactory } from '@/domain/Executables/Script/ScriptFactory'; +import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester'; +import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator'; + +describe('ScriptParser', () => { + describe('parseScript', () => { + describe('property validation', () => { + describe('validates object', () => { + // arrange + const expectedScript = createScriptDataWithCall(); + const expectedContext: ScriptErrorContext = { + type: ExecutableType.Script, + self: expectedScript, + }; + const expectedAssertion: ObjectAssertion = { + value: expectedScript, + valueName: expectedScript.name, + allowedProperties: [ + 'name', 'recommend', 'code', 'revertCode', 'call', 'docs', + ], + }; + itValidatesType( + (validatorFactory) => { + // act + new TestContext() + .withData(expectedScript) + .withValidatorFactory(validatorFactory) + .parseScript(); + // assert + return { + expectedDataToValidate: expectedScript, + expectedErrorContext: expectedContext, + assertValidation: (validator) => validator.assertObject(expectedAssertion), + }; + }, + ); + }); + describe('validates union type', () => { + // arrange + const testScenarios = generateDataValidationTestScenarios( + { + assertErrorMessage: 'Neither "call" or "code" is defined.', + expectFail: [{ + description: 'with no call or code', + data: createScriptDataWithoutCallOrCodes(), + }], + expectPass: [ + { + description: 'with call', + data: createScriptDataWithCall(), + }, + { + description: 'with code', + data: createScriptDataWithCode(), + }, + ], + }, + { + assertErrorMessage: 'Both "call" and "revertCode" are defined.', + expectFail: [{ + description: 'with both call and revertCode', + data: createScriptDataWithCall() + .withRevertCode('revert-code'), + }], + expectPass: [ + { + description: 'with call, without revertCode', + data: createScriptDataWithCall() + .withRevertCode(undefined), + }, + { + description: 'with revertCode, without call', + data: createScriptDataWithCode() + .withRevertCode('revert code'), + }, + ], + }, + { + assertErrorMessage: 'Both "call" and "code" are defined.', + expectFail: [{ + description: 'with both call and code', + data: createScriptDataWithCall() + .withCode('code'), + }], + expectPass: [ + { + description: 'with call, without code', + data: createScriptDataWithCall() + .withCode(''), + }, + { + description: 'with code, without call', + data: createScriptDataWithCode() + .withCode('code'), + }, + ], + }, + ); + testScenarios.forEach(({ + description, expectedPass, data: scriptData, expectedMessage, + }) => { + describe(description, () => { + itAsserts({ + expectedConditionResult: expectedPass, + test: (validatorFactory) => { + const expectedContext: ScriptErrorContext = { + type: ExecutableType.Script, + self: scriptData, + }; + // act + new TestContext() + .withData(scriptData) + .withValidatorFactory(validatorFactory) + .parseScript(); + // assert + expectExists(expectedMessage); + return { + expectedErrorMessage: expectedMessage, + expectedErrorContext: expectedContext, + }; + }, + }); + }); + }); + }); + }); + describe('id', () => { + it('creates ID correctly', () => { + // arrange + const expectedId: ExecutableId = 'expected-id'; + const scriptData = createScriptDataWithCode() + .withName(expectedId); + const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy(); + // act + const actualScript = new TestContext() + .withData(scriptData) + .withScriptFactory(scriptFactorySpy) + .parseScript(); + // assert + const actualId = getInitParameters(actualScript)?.executableId; + expect(actualId).to.equal(expectedId); + }); + }); + describe('name', () => { + it('parses name correctly', () => { + // arrange + const expected = 'test-expected-name'; + const scriptData = createScriptDataWithCode() + .withName(expected); + const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy(); + // act + const actualScript = new TestContext() + .withData(scriptData) + .withScriptFactory(scriptFactorySpy) + .parseScript(); + // assert + const actualName = getInitParameters(actualScript)?.name; + expect(actualName).to.equal(expected); + }); + describe('validates name', () => { + // arrange + const expectedName = 'expected script name to be validated'; + const script = createScriptDataWithCall() + .withName(expectedName); + const expectedContext: ScriptErrorContext = { + type: ExecutableType.Script, + self: script, + }; + itValidatesName((validatorFactory) => { + // act + new TestContext() + .withData(script) + .withValidatorFactory(validatorFactory) + .parseScript(); + // assert + return { + expectedNameToValidate: expectedName, + expectedErrorContext: expectedContext, + }; + }); + }); + }); + describe('docs', () => { + it('parses docs correctly', () => { + // arrange + const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com']; + const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy(); + const scriptData = createScriptDataWithCode() + .withDocs(expectedDocs); + const docsParser: DocsParser = (data) => data.docs as typeof expectedDocs; + // act + const actualScript = new TestContext() + .withData(scriptData) + .withScriptFactory(scriptFactorySpy) + .withDocsParser(docsParser) + .parseScript(); + // assert + const actualDocs = getInitParameters(actualScript)?.docs; + expect(actualDocs).to.deep.equal(expectedDocs); + }); + }); + describe('level', () => { + describe('generated `undefined` level if given absent value', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedLevel = undefined; + const scriptData = createScriptDataWithCode() + .withRecommend(absentValue); + const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy(); + // act + const actualScript = new TestContext() + .withData(scriptData) + .withScriptFactory(scriptFactorySpy) + .parseScript(); + // assert + const actualLevel = getInitParameters(actualScript)?.level; + expect(actualLevel).to.equal(expectedLevel); + }, { excludeNull: true }); + }); + it('parses level as expected', () => { + // arrange + const expectedLevel = RecommendationLevel.Standard; + const expectedName = 'level'; + const levelText = 'standard'; + const scriptData = createScriptDataWithCode() + .withRecommend(levelText); + const parserMock = new EnumParserStub() + .setup(expectedName, levelText, expectedLevel); + const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy(); + // act + const actualScript = new TestContext() + .withData(scriptData) + .withParser(parserMock) + .withScriptFactory(scriptFactorySpy) + .parseScript(); + // assert + const actualLevel = getInitParameters(actualScript)?.level; + expect(actualLevel).to.equal(expectedLevel); + }); + }); + describe('code', () => { + it('creates from script code factory', () => { + // arrange + const expectedCode = new ScriptCodeStub(); + const scriptCodeFactory: ScriptCodeFactory = () => expectedCode; + const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy(); + // act + const actualScript = new TestContext() + .withScriptCodeFactory(scriptCodeFactory) + .withScriptFactory(scriptFactorySpy) + .parseScript(); + // assert + const actualCode = getInitParameters(actualScript)?.code; + expect(expectedCode).to.equal(actualCode); + }); + describe('parses code correctly', () => { + it('parses "execute" as expected', () => { + // arrange + const expectedCode = 'expected-code'; + let actualCode: string | undefined; + const scriptCodeFactory: ScriptCodeFactory = (code) => { + actualCode = code; + return new ScriptCodeStub(); + }; + const scriptData = createScriptDataWithCode() + .withCode(expectedCode); + // act + new TestContext() + .withData(scriptData) + .withScriptCodeFactory(scriptCodeFactory) + .parseScript(); + // assert + expect(actualCode).to.equal(expectedCode); + }); + it('parses "revert" as expected', () => { + // arrange + const expectedRevertCode = 'expected-revert-code'; + const scriptData = createScriptDataWithCode() + .withRevertCode(expectedRevertCode); + let actualRevertCode: string | undefined; + const scriptCodeFactory: ScriptCodeFactory = (_, revertCode) => { + actualRevertCode = revertCode; + return new ScriptCodeStub(); + }; + // act + new TestContext() + .withData(scriptData) + .withScriptCodeFactory(scriptCodeFactory) + .parseScript(); + // assert + expect(actualRevertCode).to.equal(expectedRevertCode); + }); + }); + describe('compiler', () => { + it('compiles the code through the compiler', () => { + // arrange + const expectedCode = new ScriptCodeStub(); + const script = createScriptDataWithCode(); + const compiler = new ScriptCompilerStub() + .withCompileAbility(script, expectedCode); + const collectionUtilities = new CategoryCollectionSpecificUtilitiesStub() + .withCompiler(compiler); + const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy(); + // act + const actualScript = new TestContext() + .withData(script) + .withCollectionUtilities(collectionUtilities) + .withScriptFactory(scriptFactorySpy) + .parseScript(); + // assert + const actualCode = getInitParameters(actualScript)?.code; + expect(actualCode).to.equal(expectedCode); + }); + }); + describe('syntax', () => { + it('set from the context', () => { // tests through script validation logic + // arrange + const commentDelimiter = 'should not throw'; + const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`; + const collectionUtilities = new CategoryCollectionSpecificUtilitiesStub() + .withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter)); + const script = createScriptDataWithoutCallOrCodes() + .withCode(duplicatedCode); + // act + const act = () => new TestContext() + .withData(script) + .withCollectionUtilities(collectionUtilities); + // assert + expect(act).to.not.throw(); + }); + }); + describe('validates a expected', () => { + it('validates script with inline code (that is not compiled)', () => { + // arrange + const expectedRules = [ + NoEmptyLines, + NoDuplicatedLines, + ]; + const expectedCode = 'expected code to be validated'; + const expectedRevertCode = 'expected revert code to be validated'; + const expectedCodeCalls = [ + expectedCode, + expectedRevertCode, + ]; + const validator = new CodeValidatorStub(); + const scriptCodeFactory = createScriptCodeFactoryStub({ + scriptCode: new ScriptCodeStub() + .withExecute(expectedCode) + .withRevert(expectedRevertCode), + }); + // act + new TestContext() + .withScriptCodeFactory(scriptCodeFactory) + .withCodeValidator(validator) + .parseScript(); + // assert + validator.assertHistory({ + validatedCodes: expectedCodeCalls, + rules: expectedRules, + }); + }); + it('does not validate compiled code', () => { + // arrange + const expectedRules = []; + const expectedCodeCalls = []; + const validator = new CodeValidatorStub(); + const script = createScriptDataWithCall(); + const compiler = new ScriptCompilerStub() + .withCompileAbility(script, new ScriptCodeStub()); + const collectionUtilities = new CategoryCollectionSpecificUtilitiesStub() + .withCompiler(compiler); + // act + new TestContext() + .withData(script) + .withCodeValidator(validator) + .withCollectionUtilities(collectionUtilities) + .parseScript(); + // assert + validator.assertHistory({ + validatedCodes: expectedCodeCalls, + rules: expectedRules, + }); + }); + }); + }); + describe('script creation', () => { + it('creates script from the factory', () => { + // arrange + const expectedScript = new ScriptStub('expected-script'); + const scriptFactory: ScriptFactory = () => expectedScript; + // act + const actualScript = new TestContext() + .withScriptFactory(scriptFactory) + .parseScript(); + // assert + expect(actualScript).to.equal(expectedScript); + }); + describe('rethrows exception if script factory fails', () => { + // arrange + const givenData = createScriptDataWithCode(); + const expectedContextMessage = 'Failed to parse script.'; + const expectedError = new Error(); + const validatorFactory: ExecutableValidatorFactory = () => { + const validatorStub = new ExecutableValidatorStub(); + validatorStub.createContextualErrorMessage = (message) => message; + return validatorStub; + }; + // act & assert + itThrowsContextualError({ + throwingAction: (wrapError) => { + const factoryMock: ScriptFactory = () => { + throw expectedError; + }; + new TestContext() + .withScriptFactory(factoryMock) + .withErrorWrapper(wrapError) + .withValidatorFactory(validatorFactory) + .withData(givenData) + .parseScript(); + }, + expectedWrappedError: expectedError, + expectedContextMessage, + }); + }); + }); + }); +}); + +class TestContext { + private data: ScriptData = createScriptDataWithCode(); + + private collectionUtilities + : CategoryCollectionSpecificUtilities = new CategoryCollectionSpecificUtilitiesStub(); + + private levelParser: EnumParser = new EnumParserStub() + .setupDefaultValue(RecommendationLevel.Standard); + + private scriptFactory: ScriptFactory = createScriptFactorySpy().scriptFactorySpy; + + private codeValidator: ICodeValidator = new CodeValidatorStub(); + + private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get(); + + private validatorFactory: ExecutableValidatorFactory = createExecutableValidatorFactoryStub; + + private docsParser: DocsParser = () => ['docs']; + + private scriptCodeFactory: ScriptCodeFactory = createScriptCodeFactoryStub({ + defaultCodePrefix: TestContext.name, + }); + + public withCodeValidator(codeValidator: ICodeValidator): this { + this.codeValidator = codeValidator; + return this; + } + + public withData(data: ScriptData): this { + this.data = data; + return this; + } + + public withCollectionUtilities( + collectionUtilities: CategoryCollectionSpecificUtilities, + ): this { + this.collectionUtilities = collectionUtilities; + return this; + } + + public withParser(parser: EnumParser): this { + this.levelParser = parser; + return this; + } + + public withScriptFactory(scriptFactory: ScriptFactory): this { + this.scriptFactory = scriptFactory; + return this; + } + + public withValidatorFactory(validatorFactory: ExecutableValidatorFactory): this { + this.validatorFactory = validatorFactory; + return this; + } + + public withErrorWrapper(errorWrapper: ErrorWithContextWrapper): this { + this.errorWrapper = errorWrapper; + return this; + } + + public withScriptCodeFactory(scriptCodeFactory: ScriptCodeFactory): this { + this.scriptCodeFactory = scriptCodeFactory; + return this; + } + + public withDocsParser(docsParser: DocsParser): this { + this.docsParser = docsParser; + return this; + } + + public parseScript(): ReturnType { + return parseScript( + this.data, + this.collectionUtilities, + { + levelParser: this.levelParser, + createScript: this.scriptFactory, + codeValidator: this.codeValidator, + wrapError: this.errorWrapper, + createValidator: this.validatorFactory, + createCode: this.scriptCodeFactory, + parseDocs: this.docsParser, + }, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Validation/CodeValidator.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/CodeValidator.spec.ts new file mode 100644 index 00000000..edd8431e --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Validation/CodeValidator.spec.ts @@ -0,0 +1,182 @@ +import { describe, it, expect } from 'vitest'; +import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator'; +import { CodeValidationRuleStub } from '@tests/unit/shared/Stubs/CodeValidationRuleStub'; +import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests'; +import type { ICodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeLine'; +import type { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeValidationRule'; +import { indentText } from '@/application/Common/Text/IndentText'; + +describe('CodeValidator', () => { + describe('instance', () => { + itIsSingletonFactory({ + getter: () => CodeValidator.instance, + expectedType: CodeValidator, + }); + }); + describe('throwIfInvalid', () => { + describe('does not throw if code is absent', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const code = absentValue; + const sut = new CodeValidator(); + // act + const act = () => sut.throwIfInvalid(code, [new CodeValidationRuleStub()]); + // assert + expect(act).to.not.throw(); + }, { excludeNull: true, excludeUndefined: true }); + }); + describe('throws if rules are empty', () => { + itEachAbsentCollectionValue((absentValue) => { + // arrange + const expectedError = 'missing rules'; + const rules = absentValue; + const sut = new CodeValidator(); + // act + const act = () => sut.throwIfInvalid('code', rules); + // assert + expect(act).to.throw(expectedError); + }, { excludeUndefined: true, excludeNull: true }); + }); + describe('splits lines as expected', () => { + it('supports all line separators', () => { + // arrange + const expectedLineTexts = ['line1', 'line2', 'line3', 'line4']; + const code = 'line1\r\nline2\rline3\nline4'; + const spy = new CodeValidationRuleStub(); + const sut = new CodeValidator(); + // act + sut.throwIfInvalid(code, [spy]); + // expect + expect(spy.receivedLines).has.lengthOf(1); + const actualLineTexts = spy.receivedLines[0].map((line) => line.text); + expect(actualLineTexts).to.deep.equal(expectedLineTexts); + }); + it('uses 1-indexed line numbering', () => { + // arrange + const expectedIndexes = [1, 2, 3]; + const code = ['line1', 'line2', 'line3'].join('\n'); + const spy = new CodeValidationRuleStub(); + const sut = new CodeValidator(); + // act + sut.throwIfInvalid(code, [spy]); + // expect + expect(spy.receivedLines).has.lengthOf(1); + const actualLineIndexes = spy.receivedLines[0].map((line) => line.index); + expect(actualLineIndexes).to.deep.equal(expectedIndexes); + }); + it('counts empty lines', () => { + // arrange + const expectedTotalEmptyLines = 4; + const code = '\n'.repeat(expectedTotalEmptyLines - 1); + const spy = new CodeValidationRuleStub(); + const sut = new CodeValidator(); + // act + sut.throwIfInvalid(code, [spy]); + // expect + expect(spy.receivedLines).has.lengthOf(1); + const actualLines = spy.receivedLines[0]; + expect(actualLines).to.have.lengthOf(expectedTotalEmptyLines); + }); + it('matches texts with indexes as expected', () => { + // arrange + const expected: readonly ICodeLine[] = [ + { index: 1, text: 'first' }, + { index: 2, text: 'second' }, + ]; + const code = expected.map((line) => line.text).join('\n'); + const spy = new CodeValidationRuleStub(); + const sut = new CodeValidator(); + // act + sut.throwIfInvalid(code, [spy]); + // expect + expect(spy.receivedLines).has.lengthOf(1); + expect(spy.receivedLines[0]).to.deep.equal(expected); + }); + }); + describe('throws invalid lines as expected', () => { + it('throws with invalid line from single rule', () => { + // arrange + const errorText = 'error'; + const expectedError = new ExpectedErrorBuilder() + .withOkLine('line1') + .withErrorLine('line2', errorText) + .withOkLine('line3') + .withOkLine('line4') + .buildError(); + const code = ['line1', 'line2', 'line3', 'line4'].join('\n'); + const invalidLines: readonly IInvalidCodeLine[] = [ + { index: 2, error: errorText }, + ]; + const rule = new CodeValidationRuleStub() + .withReturnValue(invalidLines); + const noopRule = new CodeValidationRuleStub() + .withReturnValue([]); + const sut = new CodeValidator(); + // act + const act = () => sut.throwIfInvalid(code, [rule, noopRule]); + // assert + expect(act).to.throw(expectedError); + }); + it('throws with combined invalid lines from multiple rules', () => { + // arrange + const firstError = 'firstError'; + const secondError = 'firstError'; + const expectedError = new ExpectedErrorBuilder() + .withOkLine('line1') + .withErrorLine('line2', firstError) + .withOkLine('line3') + .withErrorLine('line4', secondError) + .buildError(); + const code = ['line1', 'line2', 'line3', 'line4'].join('\n'); + const firstRuleError: readonly IInvalidCodeLine[] = [ + { index: 2, error: firstError }, + ]; + const secondRuleError: readonly IInvalidCodeLine[] = [ + { index: 4, error: secondError }, + ]; + const firstRule = new CodeValidationRuleStub().withReturnValue(firstRuleError); + const secondRule = new CodeValidationRuleStub().withReturnValue(secondRuleError); + const sut = new CodeValidator(); + // act + const act = () => sut.throwIfInvalid(code, [firstRule, secondRule]); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); +}); + +class ExpectedErrorBuilder { + private lineCount = 0; + + private outputLines = new Array(); + + public withOkLine(text: string) { + return this.withNumberedLine(`✅ ${text}`); + } + + public withErrorLine(text: string, error: string) { + return this + .withNumberedLine(`❌ ${text}`) + .withLine(indentText(`⟶ ${error}`)); + } + + public buildError(): string { + return [ + 'Errors with the code.', + ...this.outputLines, + ].join('\n'); + } + + private withLine(line: string) { + this.outputLines.push(line); + return this; + } + + private withNumberedLine(text: string) { + this.lineCount += 1; + const lineNumber = `[${this.lineCount}]`; + return this.withLine(`${lineNumber} ${text}`); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Rules/CodeValidationRuleTestRunner.ts b/tests/unit/application/Parser/Executable/Script/Validation/Rules/CodeValidationRuleTestRunner.ts new file mode 100644 index 00000000..6c163300 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Validation/Rules/CodeValidationRuleTestRunner.ts @@ -0,0 +1,36 @@ +import { it, expect } from 'vitest'; +import type { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeValidationRule'; +import type { ICodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeLine'; + +interface ICodeValidationRuleTestCase { + testName: string; + codeLines: readonly string[]; + expected: readonly IInvalidCodeLine[]; + sut: ICodeValidationRule; +} + +export function testCodeValidationRule(testCases: readonly ICodeValidationRuleTestCase[]) { + for (const testCase of testCases) { + it(testCase.testName, () => { + // arrange + const { sut } = testCase; + const codeLines = createCodeLines(testCase.codeLines); + // act + const actual = sut.analyze(codeLines); + // assert + function sort(lines: readonly IInvalidCodeLine[]) { // To ignore order + return Array.from(lines).sort((a, b) => a.index - b.index); + } + expect(sort(actual)).to.deep.equal(sort(testCase.expected)); + }); + } +} + +function createCodeLines(lines: readonly string[]): ICodeLine[] { + return lines.map((lineText, index): ICodeLine => ( + { + index: index + 1, + text: lineText, + } + )); +} diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines.spec.ts new file mode 100644 index 00000000..accad9d8 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines.spec.ts @@ -0,0 +1,78 @@ +import { describe } from 'vitest'; +import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines'; +import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; +import type { IInvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeValidationRule'; +import { testCodeValidationRule } from './CodeValidationRuleTestRunner'; + +describe('NoDuplicatedLines', () => { + describe('analyze', () => { + testCodeValidationRule([ + { + testName: 'no results when code is valid', + codeLines: ['unique1', 'unique2', 'unique3', 'unique4'], + expected: [], + sut: new NoDuplicatedLines(new LanguageSyntaxStub()), + }, + { + testName: 'detects single duplicated line as expected', + codeLines: ['duplicate', 'duplicate', 'unique', 'duplicate'], + expected: expectInvalidCodeLines([1, 2, 4]), + sut: new NoDuplicatedLines(new LanguageSyntaxStub()), + }, + { + testName: 'detects multiple duplicated lines as expected', + codeLines: ['duplicate1', 'duplicate2', 'unique', 'duplicate1', 'unique2', 'duplicate2'], + expected: expectInvalidCodeLines([1, 4], [2, 6]), + sut: new NoDuplicatedLines(new LanguageSyntaxStub()), + }, + { + testName: 'common code parts: does not detect multiple common code part usages as duplicates', + codeLines: ['good', 'good', 'bad', 'bad', 'good', 'also-good', 'also-good', 'unique'], + expected: expectInvalidCodeLines([3, 4]), + sut: new NoDuplicatedLines(new LanguageSyntaxStub() + .withCommonCodeParts('good', 'also-good')), + }, + { + testName: 'common code parts: does not detect multiple common code part used in same code line as duplicates', + codeLines: ['bad', 'bad', 'good1 good2', 'good1 good2', 'good2 good1', 'good2 good1'], + expected: expectInvalidCodeLines([1, 2]), + sut: new NoDuplicatedLines(new LanguageSyntaxStub() + .withCommonCodeParts('good2', 'good1')), + }, + { + testName: 'common code parts: detects when common code parts used in conjunction with unique words', + codeLines: [ + 'common-part1', 'common-part1', 'common-part1 common-part2', 'common-part1 unique', 'common-part1 unique', + 'common-part2', 'common-part2 common-part1', 'unique common-part2', 'unique common-part2', + ], + expected: expectInvalidCodeLines([4, 5], [8, 9]), + sut: new NoDuplicatedLines(new LanguageSyntaxStub() + .withCommonCodeParts('common-part1', 'common-part2')), + }, + { + testName: 'comments: does not when lines start with comment', + codeLines: ['#abc', '#abc', 'abc', 'unique', 'abc', '//abc', '//abc', '//unique', '#unique'], + expected: expectInvalidCodeLines([3, 5]), + sut: new NoDuplicatedLines(new LanguageSyntaxStub() + .withCommentDelimiters('#', '//')), + }, + { + testName: 'comments: does when comments come after lien start', + codeLines: ['test #comment', 'test #comment', 'test2 # comment', 'test2 # comment'], + expected: expectInvalidCodeLines([1, 2], [3, 4]), + sut: new NoDuplicatedLines(new LanguageSyntaxStub() + .withCommentDelimiters('#')), + }, + ]); + }); +}); + +function expectInvalidCodeLines( + ...lines: readonly ReadonlyArray[] +): IInvalidCodeLine[] { + return lines.flatMap((occurrenceIndices): readonly IInvalidCodeLine[] => occurrenceIndices + .map((index): IInvalidCodeLine => ({ + index, + error: `Line is duplicated at line numbers ${occurrenceIndices.join(',')}.`, + }))); +} diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines.spec.ts new file mode 100644 index 00000000..03e4db7f --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines.spec.ts @@ -0,0 +1,46 @@ +import { describe } from 'vitest'; +import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines'; +import { testCodeValidationRule } from './CodeValidationRuleTestRunner'; + +describe('NoEmptyLines', () => { + describe('analyze', () => { + testCodeValidationRule([ + { + testName: 'no results when code is valid', + codeLines: ['non-empty-line1', 'none-empty-line2'], + expected: [], + sut: new NoEmptyLines(), + }, + { + testName: 'shows error for empty line', + codeLines: ['first line', '', 'third line'], + expected: [{ index: 2, error: 'Empty line' }], + sut: new NoEmptyLines(), + }, + { + testName: 'shows error for multiple empty lines', + codeLines: ['first line', '', 'third line', ''], + expected: [2, 4].map((index) => ({ index, error: 'Empty line' })), + sut: new NoEmptyLines(), + }, + { + testName: 'shows error for whitespace-only lines', + codeLines: ['first line', ' ', 'third line'], + expected: [{ index: 2, error: 'Empty line: "{whitespace}{whitespace}"' }], + sut: new NoEmptyLines(), + }, + { + testName: 'shows error for tab-only lines', + codeLines: ['first line', '\t\t', 'third line'], + expected: [{ index: 2, error: 'Empty line: "{tab}{tab}"' }], + sut: new NoEmptyLines(), + }, + { + testName: 'shows error for lines that consists of whitespace and tabs', + codeLines: ['first line', '\t \t', 'third line', ' \t '], + expected: [{ index: 2, error: 'Empty line: "{tab}{whitespace}{tab}"' }, { index: 4, error: 'Empty line: "{whitespace}{tab}{whitespace}"' }], + sut: new NoEmptyLines(), + }, + ]); + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Syntax/ConcreteSyntaxes.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/Syntax/ConcreteSyntaxes.spec.ts new file mode 100644 index 00000000..deade41f --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Validation/Syntax/ConcreteSyntaxes.spec.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; +import { ShellScriptSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ShellScriptSyntax'; +import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax'; +import { BatchFileSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/BatchFileSyntax'; + +function getSystemsUnderTest(): ILanguageSyntax[] { + return [new BatchFileSyntax(), new ShellScriptSyntax()]; +} + +describe('ConcreteSyntaxes', () => { + describe('commentDelimiters', () => { + for (const sut of getSystemsUnderTest()) { + it(`${sut.constructor.name} returns defined value`, () => { + // act + const value = sut.commentDelimiters; + // assert + expect(value); + }); + } + }); + describe('commonCodeParts', () => { + for (const sut of getSystemsUnderTest()) { + it(`${sut.constructor.name} returns defined value`, () => { + // act + const value = sut.commonCodeParts; + // assert + expect(value); + }); + } + }); +}); diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Syntax/SyntaxFactory.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/Syntax/SyntaxFactory.spec.ts new file mode 100644 index 00000000..35a4b47d --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Validation/Syntax/SyntaxFactory.spec.ts @@ -0,0 +1,14 @@ +import { describe } from 'vitest'; +import { SyntaxFactory } from '@/application/Parser/Executable/Script/Validation/Syntax/SyntaxFactory'; +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import { ShellScriptSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ShellScriptSyntax'; +import { ScriptingLanguageFactoryTestRunner } from '@tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner'; +import { BatchFileSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/BatchFileSyntax'; + +describe('SyntaxFactory', () => { + const sut = new SyntaxFactory(); + const runner = new ScriptingLanguageFactoryTestRunner() + .expectInstance(ScriptingLanguage.shellscript, ShellScriptSyntax) + .expectInstance(ScriptingLanguage.batchfile, BatchFileSyntax); + runner.testCreateMethod(sut); +}); diff --git a/tests/unit/application/Parser/Executable/Validation/DataValidationTestScenarioGenerator.ts b/tests/unit/application/Parser/Executable/Validation/DataValidationTestScenarioGenerator.ts new file mode 100644 index 00000000..e8d19384 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Validation/DataValidationTestScenarioGenerator.ts @@ -0,0 +1,36 @@ +export interface DataValidationTestScenario { + readonly description: string; + readonly data: T; + readonly expectedPass: boolean; + readonly expectedMessage?: string; +} + +export function generateDataValidationTestScenarios( + ...conditionBasedScenarios: DataValidationConditionBasedTestScenario[] +): DataValidationTestScenario[] { + return conditionBasedScenarios.flatMap((conditionScenario) => [ + conditionScenario.expectFail.map((failDefinition): DataValidationTestScenario => ({ + description: `fails: "${failDefinition.description}"`, + data: failDefinition.data, + expectedPass: false, + expectedMessage: conditionScenario.assertErrorMessage, + })), + conditionScenario.expectPass.map((passDefinition): DataValidationTestScenario => ({ + description: `passes: "${passDefinition.description}"`, + data: passDefinition.data, + expectedPass: true, + expectedMessage: conditionScenario.assertErrorMessage, + })), + ].flat()); +} + +interface DataValidationConditionBasedTestScenario { + readonly assertErrorMessage?: string; + readonly expectPass: readonly DataValidationScenarioDefinition[]; + readonly expectFail: readonly DataValidationScenarioDefinition[]; +} + +interface DataValidationScenarioDefinition { + readonly description: string; + readonly data: T; +} diff --git a/tests/unit/application/Parser/Executable/Validation/ExecutableErrorContextMessage.spec.ts b/tests/unit/application/Parser/Executable/Validation/ExecutableErrorContextMessage.spec.ts new file mode 100644 index 00000000..966abe56 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Validation/ExecutableErrorContextMessage.spec.ts @@ -0,0 +1,194 @@ +import { describe, it, expect } from 'vitest'; +import { createExecutableErrorContextStub } from '@tests/unit/shared/Stubs/ExecutableErrorContextStub'; +import { createExecutableContextErrorMessage } from '@/application/Parser/Executable/Validation/ExecutableErrorContextMessage'; +import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext'; +import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType'; +import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub'; + +describe('ExecutableErrorContextMessage', () => { + describe('createExecutableContextErrorMessage', () => { + it('includes the specified error message', () => { + // arrange + const expectedErrorMessage = 'expected error message'; + const context = new TestContext() + .withErrorMessage(expectedErrorMessage); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedErrorMessage); + }); + it('includes the type of executable', () => { + // arrange + const executableType = ExecutableType.Category; + const expectedType = ExecutableType[executableType]; + const errorContext: ExecutableErrorContext = { + type: executableType, + self: new CategoryDataStub(), + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedType); + }); + it('includes details of the self executable', () => { + // arrange + const expectedName = 'expected name'; + const selfExecutable = new CategoryDataStub() + .withName(expectedName); + const errorContext: ExecutableErrorContext = { + type: ExecutableType.Category, + self: selfExecutable, + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedName); + }); + it('includes details of the parent category', () => { + // arrange + const expectedName = 'expected parent name'; + const parentCategoryData = new CategoryDataStub() + .withName(expectedName); + const errorContext: ExecutableErrorContext = { + type: ExecutableType.Category, + self: new CategoryDataStub(), + parentCategory: parentCategoryData, + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedName); + }); + it('constructs the complete message format correctly', () => { + // arrange + const errorMessage = 'expected error message'; + const expectedName = 'expected name'; + const expectedFormat = new RegExp(`^${escapeRegExp(errorMessage)}\\s+Executable:\\s+{\\s+"name":\\s+"${escapeRegExp(expectedName)}"\\s+}`); + const errorContext: ExecutableErrorContext = { + self: { + name: expectedName, + } as unknown as ExecutableErrorContext['self'], + }; + const context = new TestContext() + .withErrorContext(errorContext) + .withErrorMessage(errorMessage); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.match(expectedFormat); + }); + describe('output trimming', () => { + const totalLongTextDataCharacters = 5000; + const expectedTrimmedText = '[Rest of the executable trimmed]'; + const longName = 'a'.repeat(totalLongTextDataCharacters); + const testScenarios: readonly { + readonly description: string; + readonly errorContext: ExecutableErrorContext; + } [] = [ + { + description: 'long text from parent category data', + errorContext: { + type: ExecutableType.Category, + self: new CategoryDataStub(), + parentCategory: new CategoryDataStub().withName(longName), + }, + }, + { + description: 'long text from self executable data', + errorContext: { + type: ExecutableType.Category, + self: new CategoryDataStub().withName(longName), + }, + }, + ]; + testScenarios.forEach(({ + description, errorContext, + }) => { + it(description, () => { + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedTrimmedText); + expect(actualMessage).to.have.length.lessThan(totalLongTextDataCharacters); + }); + }); + }); + describe('missing data handling', () => { + it('generates a message when the executable type is undefined', () => { + // arrange + const errorContext: ExecutableErrorContext = { + type: undefined, + self: new CategoryDataStub(), + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.have.length.greaterThan(0); + }); + it('generates a message when executable data is missing', () => { + // arrange + const errorContext: ExecutableErrorContext = { + type: undefined, + self: undefined as unknown as ExecutableErrorContext['self'], + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.have.length.greaterThan(0); + }); + it('generates a message when parent category is missing', () => { + // arrange + const errorContext: ExecutableErrorContext = { + type: undefined, + self: new CategoryDataStub(), + parentCategory: undefined, + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.have.length.greaterThan(0); + }); + }); + }); +}); + +class TestContext { + private errorMessage = `[${TestContext.name}] error message`; + + private errorContext: ExecutableErrorContext = createExecutableErrorContextStub(); + + public withErrorMessage(errorMessage: string): this { + this.errorMessage = errorMessage; + return this; + } + + public withErrorContext(context: ExecutableErrorContext): this { + this.errorContext = context; + return this; + } + + public createExecutableContextErrorMessage() { + return createExecutableContextErrorMessage( + this.errorMessage, + this.errorContext, + ); + } +} + +function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');// $& means the whole matched string +} diff --git a/tests/unit/application/Parser/Executable/Validation/ExecutableValidationTester.ts b/tests/unit/application/Parser/Executable/Validation/ExecutableValidationTester.ts new file mode 100644 index 00000000..6dc06455 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Validation/ExecutableValidationTester.ts @@ -0,0 +1,192 @@ +import { it } from 'vitest'; +import type { ExecutableValidator, ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator'; +import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext'; +import { ExecutableValidatorStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; +import type { FunctionKeys } from '@/TypeHelpers'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import { expectDeepIncludes } from '@tests/shared/Assertions/ExpectDeepIncludes'; +import { indentText } from '@/application/Common/Text/IndentText'; + +type ValidationTestFunction = ( + factory: ExecutableValidatorFactory, +) => TExpectation; + +interface ValidNameExpectation { + readonly expectedNameToValidate: string; + readonly expectedErrorContext: ExecutableErrorContext; +} + +export function itValidatesName( + test: ValidationTestFunction, +) { + it('validates for name', () => { + // arrange + const validator = new ExecutableValidatorStub(); + const factoryStub: ExecutableValidatorFactory = () => validator; + // act + test(factoryStub); + // assert + const call = validator.callHistory.find((c) => c.methodName === 'assertValidName'); + expectExists(call); + }); + it('validates for name with correct name', () => { + // arrange + const validator = new ExecutableValidatorStub(); + const factoryStub: ExecutableValidatorFactory = () => validator; + // act + const expectation = test(factoryStub); + // assert + const expectedName = expectation.expectedNameToValidate; + const names = validator.callHistory + .filter((c) => c.methodName === 'assertValidName') + .flatMap((c) => c.args[0]); + expect(names).to.include(expectedName); + }); + it('validates for name with correct context', () => { + expectCorrectContextForFunctionCall({ + methodName: 'assertValidName', + act: test, + expectContext: (expectation) => expectation.expectedErrorContext, + }); + }); +} + +interface TypeAssertionExpectation { + readonly expectedErrorContext: ExecutableErrorContext; + readonly assertValidation: (validator: TypeValidatorStub) => void; +} + +export function itValidatesType( + test: ValidationTestFunction, +) { + it('validates type', () => { + // arrange + const validator = new ExecutableValidatorStub(); + const factoryStub: ExecutableValidatorFactory = () => validator; + // act + test(factoryStub); + // assert + const call = validator.callHistory.find((c) => c.methodName === 'assertType'); + expectExists(call); + }); + it('validates type using specified validator', () => { + // arrange + const typeValidator = new TypeValidatorStub(); + const validator = new ExecutableValidatorStub(); + const factoryStub: ExecutableValidatorFactory = () => validator; + // act + const expectation = test(factoryStub); + // assert + const calls = validator.callHistory.filter((c) => c.methodName === 'assertType'); + const args = calls.map((c) => c.args as Parameters); + const validateFunctions = args.flatMap((c) => c[0]); + validateFunctions.forEach((validate) => validate(typeValidator)); + expectation.assertValidation(typeValidator); + }); + it('validates type with correct context', () => { + expectCorrectContextForFunctionCall({ + methodName: 'assertType', + act: test, + expectContext: (expectation) => expectation.expectedErrorContext, + }); + }); +} + +interface AssertionExpectation { + readonly expectedErrorMessage: string; + readonly expectedErrorContext: ExecutableErrorContext; +} + +export function itAsserts( + testScenario: { + readonly test: ValidationTestFunction, + readonly expectedConditionResult: boolean; + }, +) { + it('asserts with correct message', () => { + // arrange + const validator = new ExecutableValidatorStub() + .withAssertThrowsOnFalseCondition(false); + const factoryStub: ExecutableValidatorFactory = () => validator; + // act + const expectation = testScenario.test(factoryStub); + // assert + const expectedError = expectation.expectedErrorMessage; + const calls = validator.callHistory.filter((c) => c.methodName === 'assert'); + const actualMessages = calls.map((call) => { + const [, message] = call.args; + return message; + }); + expect(actualMessages).to.include(expectedError, formatAssertionMessage([ + 'Assertion failed: The expected error message was not triggered.', + `Expected: "${expectedError}"`, + 'Actual messages (none match expected):', + indentText(actualMessages.map((message) => `- ${message}`).join('\n')), + ])); + }); + it('asserts with correct context', () => { + expectCorrectContextForFunctionCall({ + methodName: 'assert', + act: testScenario.test, + expectContext: (expectation) => expectation.expectedErrorContext, + }); + }); + it('asserts with correct condition result', () => { + // arrange + const expectedEvaluationResult = testScenario.expectedConditionResult; + const validator = new ExecutableValidatorStub() + .withAssertThrowsOnFalseCondition(false); + const factoryStub: ExecutableValidatorFactory = () => validator; + // act + const expectation = testScenario.test(factoryStub); + // assert + const assertCalls = validator.callHistory + .filter((call) => call.methodName === 'assert'); + expect(assertCalls).to.have.length.greaterThan(0); + const assertCallsWithMessage = assertCalls + .filter((call) => { + const [, message] = call.args; + return message === expectation.expectedErrorMessage; + }); + expect(assertCallsWithMessage).to.have.length.greaterThan(0); + const evaluationResults = assertCallsWithMessage + .map((call) => { + const [predicate] = call.args; + return predicate as (() => boolean); + }) + .map((predicate) => predicate()); + expect(evaluationResults).to.include(expectedEvaluationResult); + }); +} + +function expectCorrectContextForFunctionCall(testScenario: { + methodName: FunctionKeys, + act: ValidationTestFunction, + expectContext: (actionResult: T) => ExecutableErrorContext, +}) { + // arrange + const { methodName } = testScenario; + const createdValidators = new Array<{ + readonly validator: ExecutableValidatorStub; + readonly context: ExecutableErrorContext; + }>(); + const factoryStub: ExecutableValidatorFactory = (context) => { + const validator = new ExecutableValidatorStub() + .withAssertThrowsOnFalseCondition(false); + createdValidators.push(({ + validator, + context, + })); + return validator; + }; + // act + const actionResult = testScenario.act(factoryStub); + // assert + const expectedContext = testScenario.expectContext(actionResult); + const providedContexts = createdValidators + .filter((v) => v.validator.callHistory.find((c) => c.methodName === methodName)) + .map((v) => v.context); + expectDeepIncludes(providedContexts, expectedContext); +} diff --git a/tests/unit/application/Parser/Executable/Validation/ExecutableValidator.spec.ts b/tests/unit/application/Parser/Executable/Validation/ExecutableValidator.spec.ts new file mode 100644 index 00000000..485a826a --- /dev/null +++ b/tests/unit/application/Parser/Executable/Validation/ExecutableValidator.spec.ts @@ -0,0 +1,248 @@ +import { describe, it, expect } from 'vitest'; +import { createExecutableErrorContextStub } from '@tests/unit/shared/Stubs/ExecutableErrorContextStub'; +import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext'; +import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector'; +import { ContextualExecutableValidator, createExecutableDataValidator, type ExecutableValidator } from '@/application/Parser/Executable/Validation/ExecutableValidator'; +import type { ExecutableContextErrorMessageCreator } from '@/application/Parser/Executable/Validation/ExecutableErrorContextMessage'; +import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import type { TypeValidator } from '@/application/Parser/Common/TypeValidator'; + +describe('createExecutableDataValidator', () => { + it(`returns an instance of ${ContextualExecutableValidator.name}`, () => { + // arrange + const context = createExecutableErrorContextStub(); + // act + const validator = createExecutableDataValidator(context); + // assert + expect(validator).to.be.instanceOf(ContextualExecutableValidator); + }); +}); + +describe('ContextualExecutableValidator', () => { + describe('assertValidName', () => { + describe('throws when name is invalid', () => { + // arrange + const testScenarios: readonly { + readonly description: string; + readonly invalidName: unknown; + readonly expectedMessage: string; + }[] = [ + ...getAbsentStringTestCases().map((testCase) => ({ + description: `missing name (${testCase.valueName})`, + invalidName: testCase.absentValue, + expectedMessage: 'missing name', + })), + { + description: 'invalid type', + invalidName: 33, + expectedMessage: 'Name (33) is not a string but number.', + }, + ]; + testScenarios.forEach(({ description, invalidName, expectedMessage }) => { + describe(`given "${description}"`, () => { + itThrowsCorrectly({ + // act + throwingAction: (sut) => { + sut.assertValidName(invalidName as string); + }, + // assert + expectedMessage, + }); + }); + }); + }); + it('does not throw when name is valid', () => { + // arrange + const validName = 'validName'; + const sut = new ValidatorBuilder() + .build(); + // act + const act = () => sut.assertValidName(validName); + // assert + expect(act).to.not.throw(); + }); + }); + describe('assertType', () => { + describe('rethrows when action throws', () => { + // arrange + const expectedMessage = 'Error thrown by action'; + itThrowsCorrectly({ + // act + throwingAction: (sut: ExecutableValidator) => { + sut.assertType(() => { + throw new Error(expectedMessage); + }); + }, + // assert + expectedMessage, + }); + }); + it('provides correct validator', () => { + // arrange + const expectedValidator = new TypeValidatorStub(); + const sut = new ValidatorBuilder() + .withTypeValidator(expectedValidator) + .build(); + let actualValidator: TypeValidator | undefined; + // act + sut.assertType((validator) => { + actualValidator = validator; + }); + // assert + expect(expectedValidator).to.equal(actualValidator); + }); + it('does not throw if action does not throw', () => { + // arrange + const sut = new ValidatorBuilder() + .build(); + // act + const act = () => sut.assertType(() => { /* Does not throw */ }); + // assert + expect(act).to.not.throw(); + }); + }); + describe('assert', () => { + describe('throws if validation fails', () => { + const falsePredicate = () => false; + const expectedErrorMessage = 'expected error'; + // assert + itThrowsCorrectly({ + // act + throwingAction: (sut: ExecutableValidator) => { + sut.assert(falsePredicate, expectedErrorMessage); + }, + // assert + expectedMessage: expectedErrorMessage, + }); + }); + it('does not throw if validation succeeds', () => { + // arrange + const truePredicate = () => true; + const sut = new ValidatorBuilder() + .build(); + // act + const act = () => sut.assert(truePredicate, 'ignored error'); + // assert + expect(act).to.not.throw(); + }); + }); + describe('createContextualErrorMessage', () => { + it('creates using the correct error message', () => { + // arrange + const expectedErrorMessage = 'expected error'; + const errorMessageBuilder: ExecutableContextErrorMessageCreator = (message) => message; + const sut = new ValidatorBuilder() + .withErrorMessageCreator(errorMessageBuilder) + .build(); + // act + const actualErrorMessage = sut.createContextualErrorMessage(expectedErrorMessage); + // assert + expect(actualErrorMessage).to.equal(expectedErrorMessage); + }); + it('creates using the correct context', () => { + // arrange + const expectedContext = createExecutableErrorContextStub(); + let actualContext: ExecutableErrorContext | undefined; + const errorMessageBuilder: ExecutableContextErrorMessageCreator = (_, context) => { + actualContext = context; + return ''; + }; + const sut = new ValidatorBuilder() + .withContext(expectedContext) + .withErrorMessageCreator(errorMessageBuilder) + .build(); + // act + sut.createContextualErrorMessage('unimportant'); + // assert + expect(actualContext).to.equal(expectedContext); + }); + }); +}); + +type ValidationThrowingFunction = ( + sut: ContextualExecutableValidator, +) => void; + +interface ValidationThrowingTestScenario { + readonly throwingAction: ValidationThrowingFunction, + readonly expectedMessage: string; +} + +function itThrowsCorrectly( + testScenario: ValidationThrowingTestScenario, +): void { + it('throws an error', () => { + // arrange + const expectedErrorMessage = 'Injected error message'; + const errorMessageBuilder: ExecutableContextErrorMessageCreator = () => expectedErrorMessage; + const sut = new ValidatorBuilder() + .withErrorMessageCreator(errorMessageBuilder) + .build(); + // act + const action = () => testScenario.throwingAction(sut); + // assert + expect(action).to.throw(); + }); + it('throws with the correct error message', () => { + // arrange + const expectedErrorMessage = testScenario.expectedMessage; + const errorMessageBuilder: ExecutableContextErrorMessageCreator = (message) => message; + const sut = new ValidatorBuilder() + .withErrorMessageCreator(errorMessageBuilder) + .build(); + // act + const action = () => testScenario.throwingAction(sut); + // assert + const actualErrorMessage = collectExceptionMessage(action); + expect(actualErrorMessage).to.equal(expectedErrorMessage); + }); + it('throws with the correct context', () => { + // arrange + const expectedContext = createExecutableErrorContextStub(); + const serializeContext = (context: ExecutableErrorContext) => JSON.stringify(context); + const errorMessageBuilder: + ExecutableContextErrorMessageCreator = (_, context) => serializeContext(context); + const sut = new ValidatorBuilder() + .withContext(expectedContext) + .withErrorMessageCreator(errorMessageBuilder) + .build(); + // act + const action = () => testScenario.throwingAction(sut); + // assert + const expectedSerializedContext = serializeContext(expectedContext); + const actualSerializedContext = collectExceptionMessage(action); + expect(expectedSerializedContext).to.equal(actualSerializedContext); + }); +} + +class ValidatorBuilder { + private errorContext: ExecutableErrorContext = createExecutableErrorContextStub(); + + private errorMessageCreator: ExecutableContextErrorMessageCreator = () => `[${ValidatorBuilder.name}] stub error message`; + + private typeValidator: TypeValidator = new TypeValidatorStub(); + + public withErrorMessageCreator(errorMessageCreator: ExecutableContextErrorMessageCreator): this { + this.errorMessageCreator = errorMessageCreator; + return this; + } + + public withContext(errorContext: ExecutableErrorContext): this { + this.errorContext = errorContext; + return this; + } + + public withTypeValidator(typeValidator: TypeValidator): this { + this.typeValidator = typeValidator; + return this; + } + + public build(): ContextualExecutableValidator { + return new ContextualExecutableValidator( + this.errorContext, + this.errorMessageCreator, + this.typeValidator, + ); + } +} diff --git a/tests/unit/application/Parser/ProjectDetailsParser.spec.ts b/tests/unit/application/Parser/ProjectDetailsParser.spec.ts new file mode 100644 index 00000000..7b54cb55 --- /dev/null +++ b/tests/unit/application/Parser/ProjectDetailsParser.spec.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from 'vitest'; +import { parseProjectDetails, type ProjectDetailsFactory } from '@/application/Parser/ProjectDetailsParser'; +import { AppMetadataStub } from '@tests/unit/shared/Stubs/AppMetadataStub'; +import type { PropertyKeys } from '@/TypeHelpers'; +import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'; +import { Version } from '@/domain/Version'; + +describe('ProjectDetailsParser', () => { + describe('parseProjectDetails', () => { + it('returns expected instance', () => { + // arrange + const expectedInformation = new ProjectDetailsStub(); + const factoryMock = () => expectedInformation; + // act + const actualInformation = parseProjectDetails(new AppMetadataStub(), factoryMock); + // assert + expect(expectedInformation).to.equal(actualInformation); + }); + describe('default behavior does not throw', () => { + it('without metadata', () => { + // arrange + const metadataFactory = undefined; + const projectDetailsFactory = new ProjectDetailsFactoryStub().getStub(); + // act + const act = () => parseProjectDetails(metadataFactory, projectDetailsFactory); + // expectS + expect(act).to.not.throw(); + }); + it('without projectDetailsFactory', () => { + // arrange + const metadataFactory = new AppMetadataStub(); + const projectDetailsFactory = undefined; + // act + const act = () => parseProjectDetails(metadataFactory, projectDetailsFactory); + // expect + expect(act).to.not.throw(); + }); + }); + describe('parses metadata correctly', () => { + interface MetadataTestScenario { + readonly setMetadata: (appMetadataStub: AppMetadataStub, value: string) => AppMetadataStub; + readonly expectedValue: string; + readonly getActualValue: (projectDetailsFactory: ProjectDetailsFactoryStub) => string; + } + const testScenarios: { + [K in PropertyKeys]: MetadataTestScenario + } = { + name: { + setMetadata: (metadata, value) => metadata.witName(value), + expectedValue: 'expected-app-name', + getActualValue: (projectDetailsFactory) => projectDetailsFactory.name, + }, + version: { + setMetadata: (metadata, value) => metadata.withVersion(value), + expectedValue: '0.11.3', + getActualValue: (projectDetailsFactory) => projectDetailsFactory.version.toString(), + }, + slogan: { + setMetadata: (metadata, value) => metadata.withSlogan(value), + expectedValue: 'expected-slogan', + getActualValue: (projectDetailsFactory) => projectDetailsFactory.slogan, + }, + repositoryUrl: { + setMetadata: (metadata, value) => metadata.withRepositoryUrl(value), + expectedValue: 'https://expected-repository.url', + getActualValue: (projectDetailsFactory) => projectDetailsFactory.repositoryUrl, + }, + homepage: { + setMetadata: (metadata, value) => metadata.withHomepageUrl(value), + expectedValue: 'https://expected.sexy', + getActualValue: (projectDetailsFactory) => projectDetailsFactory.homepage, + }, + }; + Object.entries(testScenarios).forEach(([propertyName, { + expectedValue, setMetadata, getActualValue, + }]) => { + it(propertyName, () => { + // act + const metadata = setMetadata(new AppMetadataStub(), expectedValue); + const projectDetailsFactoryStub = new ProjectDetailsFactoryStub(); + // act + parseProjectDetails(metadata, projectDetailsFactoryStub.getStub()); + // assert + const actual = getActualValue(projectDetailsFactoryStub); + expect(actual).to.be.equal(expectedValue); + }); + }); + }); + }); +}); + +class ProjectDetailsFactoryStub { + public name: string; + + public version: Version; + + public slogan: string; + + public repositoryUrl: string; + + public homepage: string; + + public getStub(): ProjectDetailsFactory { + return (name, version, slogan, repositoryUrl, homepage) => { + this.name = name; + this.version = version; + this.slogan = slogan; + this.repositoryUrl = repositoryUrl; + this.homepage = homepage; + return new ProjectDetailsStub(); + }; + } +} diff --git a/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts b/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts new file mode 100644 index 00000000..ff6c5b30 --- /dev/null +++ b/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts @@ -0,0 +1,120 @@ +import { describe, it, expect } from 'vitest'; +import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler'; +import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'; +import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import { substituteCode } from '@/application/Parser/ScriptingDefinition/CodeSubstituter'; +import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; +import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { FunctionCallArgumentFactoryStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub'; + +describe('CodeSubstituter', () => { + describe('substituteCode', () => { + describe('throws if code is empty', () => { + itEachAbsentStringValue((emptyCode) => { + // arrange + const expectedError = 'missing code'; + const context = new TestContext() + .withCode(emptyCode); + // act + const act = () => context.substitute(); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + describe('substitutes parameters as expected values', () => { + // arrange + const projectDetails = new ProjectDetailsStub(); + const date = new Date(); + const testCases: Array<{ parameter: string, argument: string }> = [ + { + parameter: 'homepage', + argument: projectDetails.homepage, + }, + { + parameter: 'version', + argument: projectDetails.version.toString(), + }, + { + parameter: 'date', + argument: date.toUTCString(), + }, + ]; + for (const testCase of testCases) { + it(`substitutes ${testCase.parameter} as expected`, () => { + const compilerStub = new ExpressionsCompilerStub(); + const context = new TestContext() + .withCompiler(compilerStub) + .withDate(date) + .withProjectDetails(projectDetails); + // act + context.substitute(); + // assert + expect(compilerStub.callHistory).to.have.lengthOf(1); + const parameters = compilerStub.callHistory[0].args[1]; + expect(parameters.hasArgument(testCase.parameter)); + const { argumentValue } = parameters.getArgument(testCase.parameter); + expect(argumentValue).to.equal(testCase.argument); + }); + } + }); + it('returns code as it is', () => { + // arrange + const expected = 'expected-code'; + const compilerStub = new ExpressionsCompilerStub(); + const context = new TestContext() + .withCompiler(compilerStub) + .withCode(expected); + // act + context.substitute(); + // assert + expect(compilerStub.callHistory).to.have.lengthOf(1); + expect(compilerStub.callHistory[0].args[0]).to.equal(expected); + }); + }); +}); + +class TestContext { + private compiler: IExpressionsCompiler = new ExpressionsCompilerStub(); + + private date = new Date(); + + private code = `[${TestContext.name}] default code for testing`; + + private projectDetails: ProjectDetails = new ProjectDetailsStub(); + + private callArgumentFactory + : FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory; + + public withCompiler(compiler: IExpressionsCompiler): this { + this.compiler = compiler; + return this; + } + + public withDate(date: Date): this { + this.date = date; + return this; + } + + public withCode(code: string): this { + this.code = code; + return this; + } + + public withProjectDetails(projectDetails: ProjectDetails): this { + this.projectDetails = projectDetails; + return this; + } + + public substitute(): ReturnType { + return substituteCode( + this.code, + this.projectDetails, + { + compiler: this.compiler, + provideDate: () => this.date, + createCallArgument: this.callArgumentFactory, + }, + ); + } +} diff --git a/tests/unit/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.spec.ts b/tests/unit/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.spec.ts new file mode 100644 index 00000000..e6ebea13 --- /dev/null +++ b/tests/unit/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.spec.ts @@ -0,0 +1,146 @@ +import { describe, it, expect } from 'vitest'; +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import type { EnumParser } from '@/application/Common/Enum'; +import type { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter'; +import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'; +import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub'; +import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub'; +import { CodeSubstituterStub } from '@tests/unit/shared/Stubs/CodeSubstituterStub'; +import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser'; +import type { ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import type { ScriptingDefinitionData } from '@/application/collections/'; +import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; + +describe('ScriptingDefinitionParser', () => { + describe('parseScriptingDefinition', () => { + it('validates data', () => { + // arrange + const data = new ScriptingDefinitionDataStub(); + const expectedAssertion: ObjectAssertion = { + value: data, + valueName: 'scripting definition', + allowedProperties: ['language', 'startCode', 'endCode'], + }; + const validatorStub = new TypeValidatorStub(); + const context = new TestContext() + .withTypeValidator(validatorStub) + .withData(data); + // act + context.parseScriptingDefinition(); + // assert + validatorStub.assertObject(expectedAssertion); + }); + describe('language', () => { + it('parses as expected', () => { + // arrange + const expectedLanguage = ScriptingLanguage.batchfile; + const languageText = 'batchfile'; + const expectedName = 'language'; + const definition = new ScriptingDefinitionDataStub() + .withLanguage(languageText); + const parserMock = new EnumParserStub() + .setup(expectedName, languageText, expectedLanguage); + const context = new TestContext() + .withParser(parserMock) + .withData(definition); + // act + const actual = context.parseScriptingDefinition(); + // assert + expect(actual.language).to.equal(expectedLanguage); + }); + }); + describe('substitutes code as expected', () => { + // arrange + const code = 'hello'; + const expected = 'substituted'; + const testScenarios: readonly { + readonly description: string; + getActualValue(result: IScriptingDefinition): string; + readonly data: ScriptingDefinitionData; + }[] = [ + { + description: 'startCode', + getActualValue: (result: IScriptingDefinition) => result.startCode, + data: new ScriptingDefinitionDataStub() + .withStartCode(code), + }, + { + description: 'endCode', + getActualValue: (result: IScriptingDefinition) => result.endCode, + data: new ScriptingDefinitionDataStub() + .withEndCode(code), + }, + ]; + testScenarios.forEach(({ + description, data, getActualValue, + }) => { + it(description, () => { + const projectDetails = new ProjectDetailsStub(); + const substituterMock = new CodeSubstituterStub() + .setup(code, projectDetails, expected); + const context = new TestContext() + .withData(data) + .withProjectDetails(projectDetails) + .withSubstituter(substituterMock.substitute); + // act + const definition = context.parseScriptingDefinition(); + // assert + const actual = getActualValue(definition); + expect(actual).to.equal(expected); + }); + }); + }); + }); +}); + +class TestContext { + private languageParser: EnumParser = new EnumParserStub() + .setupDefaultValue(ScriptingLanguage.shellscript); + + private codeSubstituter: CodeSubstituter = new CodeSubstituterStub().substitute; + + private validator: TypeValidator = new TypeValidatorStub(); + + private data: ScriptingDefinitionData = new ScriptingDefinitionDataStub(); + + private projectDetails: ProjectDetails = new ProjectDetailsStub(); + + public withData(data: ScriptingDefinitionData): this { + this.data = data; + return this; + } + + public withProjectDetails(projectDetails: ProjectDetails): this { + this.projectDetails = projectDetails; + return this; + } + + public withParser(parser: EnumParser): this { + this.languageParser = parser; + return this; + } + + public withSubstituter(substituter: CodeSubstituter): this { + this.codeSubstituter = substituter; + return this; + } + + public withTypeValidator(validator: TypeValidator): this { + this.validator = validator; + return this; + } + + public parseScriptingDefinition() { + return parseScriptingDefinition( + this.data, + this.projectDetails, + { + languageParser: this.languageParser, + codeSubstituter: this.codeSubstituter, + validator: this.validator, + }, + ); + } +} diff --git a/tests/unit/application/collections/NoUnintentedInlining.spec.ts b/tests/unit/application/collections/NoUnintentedInlining.spec.ts new file mode 100644 index 00000000..2ac6aa26 --- /dev/null +++ b/tests/unit/application/collections/NoUnintentedInlining.spec.ts @@ -0,0 +1,82 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { resolve, join, basename } from 'node:path'; +import { describe, it, expect } from 'vitest'; +import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; + +/* + A common mistake when working with yaml files to forget mentioning that a value should + be interpreted as multi-line string using "|". + E.g. + ``` + code: |- + echo Hello + echo World + ``` + If "|" is missing then the code is inlined like `echo Hello echo World``, which can be + unintended. This test checks for similar issues in collection yaml files. + These tests can be considered as "linter" more than "unit-test" and therefore can lead + to false-positives. +*/ +describe('collection files to have no unintended inlining', () => { + // arrange + const testCases = createTestCases('src/application/collections/'); + for (const testCase of testCases) { + it(`${testCase.name}`, async () => { + // act + const lines = await findBadLineNumbers(testCase.content); + // assert + expect(lines).to.be.have.lengthOf(0, formatAssertionMessage([ + 'Did you intend to have multi-lined string in lines: ', + lines.map(((line) => line.toString())).join(', '), + ])); + }); + } +}); + +async function findBadLineNumbers(fileContent: string): Promise { + return [ + ...findLineNumbersEndingWith(fileContent, 'revertCode:'), + ...findLineNumbersEndingWith(fileContent, 'code:'), + ]; +} + +function findLineNumbersEndingWith(content: string, ending: string): number[] { + sanityCheck(content, ending); + return splitTextIntoLines(content) + .map((line, index) => ({ text: line, index })) + .filter((line) => line.text.trim().endsWith(ending)) + .map((line) => line.index + 1 /* first line is 1, not 0 */); +} + +function sanityCheck(content: string, ending: string): void { + if (!content) { + throw new Error('File content is empty, is the file loaded correctly?'); + } + if (!content.includes(ending)) { + throw new Error( + `File does not contain string "${ending}" string at all.` + + `Did the word "${ending}" change? Or is this sanity check wrong?`, + ); + } +} + +interface ITestCase { + name: string; + content: string; +} +function createTestCases(collectionsDirFromRoot: string): ITestCase[] { + const collectionsDir = resolve(`./${collectionsDirFromRoot}`); + const fileNames = readdirSync(collectionsDir); + if (fileNames.length === 0) { + throw new Error(`Could not find any collection in ${collectionsDir}`); + } + const collectionFilePaths = fileNames + .filter((name) => !name.startsWith('.')) + .filter((name) => name.endsWith('.yaml')) + .map((name) => join(collectionsDir, name)); + return collectionFilePaths.map((path) => ({ + name: basename(path), + content: readFileSync(path, 'utf-8'), + })); +} diff --git a/tests/unit/application/collections/raw-loader.d.ts b/tests/unit/application/collections/raw-loader.d.ts new file mode 100644 index 00000000..d52f28bc --- /dev/null +++ b/tests/unit/application/collections/raw-loader.d.ts @@ -0,0 +1,4 @@ +declare module 'raw-loader!@/*' { + const contents: string; + export default contents; +} diff --git a/tests/unit/domain/Application.spec.ts b/tests/unit/domain/Application.spec.ts new file mode 100644 index 00000000..a408a6b8 --- /dev/null +++ b/tests/unit/domain/Application.spec.ts @@ -0,0 +1,111 @@ +import { describe, it, expect } from 'vitest'; +import { Application } from '@/domain/Application'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; +import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('Application', () => { + describe('getCollection', () => { + it('throws if not found', () => { + // arrange + const missingOs = OperatingSystem.Android; + const expectedError = `Operating system "${OperatingSystem[missingOs]}" is not defined in application`; + const projectDetails = new ProjectDetailsStub(); + const collections = [new CategoryCollectionStub().withOs(OperatingSystem.Windows)]; + // act + const sut = new Application(projectDetails, collections); + const act = () => sut.getCollection(missingOs); + // assert + expect(act).to.throw(expectedError); + }); + it('returns expected when multiple collections exist', () => { + // arrange + const os = OperatingSystem.Windows; + const expected = new CategoryCollectionStub().withOs(os); + const projectDetails = new ProjectDetailsStub(); + const collections = [expected, new CategoryCollectionStub().withOs(OperatingSystem.Android)]; + // act + const sut = new Application(projectDetails, collections); + const actual = sut.getCollection(os); + // assert + expect(actual).to.equals(expected); + }); + }); + describe('ctor', () => { + describe('projectDetails', () => { + it('sets as expected', () => { + // arrange + const expectedProjectDetails = new ProjectDetailsStub(); + const collections = [new CategoryCollectionStub()]; + // act + const sut = new Application(expectedProjectDetails, collections); + // assert + expect(sut.projectDetails).to.equal(expectedProjectDetails); + }); + }); + describe('collections', () => { + describe('throws on invalid value', () => { + // arrange + const testCases: readonly { + readonly name: string, + readonly expectedError: string, + readonly value: readonly ICategoryCollection[], + }[] = [ + ...getAbsentCollectionTestCases( + { + excludeUndefined: true, + excludeNull: true, + }, + ).map((testCase) => ({ + name: `empty collection: ${testCase.valueName}`, + expectedError: 'missing collections', + value: testCase.absentValue, + })), + { + name: 'two collections with same OS', + expectedError: 'multiple collections with same os: windows', + value: [ + new CategoryCollectionStub().withOs(OperatingSystem.Windows), + new CategoryCollectionStub().withOs(OperatingSystem.Windows), + new CategoryCollectionStub().withOs(OperatingSystem.BlackBerry10), + ], + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + const projectDetails = new ProjectDetailsStub(); + const collections = testCase.value; + // act + const act = () => new Application(projectDetails, collections); + // assert + expect(act).to.throw(testCase.expectedError); + }); + } + }); + it('sets as expected', () => { + // arrange + const projectDetails = new ProjectDetailsStub(); + const expected = [new CategoryCollectionStub()]; + // act + const sut = new Application(projectDetails, expected); + // assert + expect(sut.collections).to.equal(expected); + }); + }); + }); + describe('getSupportedOsList', () => { + it('returns expected', () => { + // arrange + const expected = [OperatingSystem.Windows, OperatingSystem.macOS]; + const projectDetails = new ProjectDetailsStub(); + const collections = expected.map((os) => new CategoryCollectionStub().withOs(os)); + // act + const sut = new Application(projectDetails, collections); + const actual = sut.getSupportedOsList(); + // assert + expect(actual).to.deep.equal(expected); + }); + }); +}); diff --git a/tests/unit/domain/Collection/CategoryCollection.spec.ts b/tests/unit/domain/Collection/CategoryCollection.spec.ts new file mode 100644 index 00000000..e7310732 --- /dev/null +++ b/tests/unit/domain/Collection/CategoryCollection.spec.ts @@ -0,0 +1,248 @@ +import { describe, it, expect } from 'vitest'; +import type { Category } from '@/domain/Executables/Category/Category'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; +import { getEnumValues } from '@/application/Common/Enum'; +import { CategoryCollection } from '@/domain/Collection/CategoryCollection'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; +import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner'; + +describe('CategoryCollection', () => { + describe('getScriptsByLevel', () => { + it('filters out scripts without levels', () => { + // arrange + const recommendationLevels = getEnumValues(RecommendationLevel); + const scriptsWithLevels = recommendationLevels.map( + (level, index) => new ScriptStub(`Script${index}`).withLevel(level), + ); + const toIgnore = new ScriptStub('script-to-ignore').withLevel(undefined); + for (const currentLevel of recommendationLevels) { + const category = new CategoryStub('parent-action') + .withScripts(...scriptsWithLevels) + .withScript(toIgnore); + const sut = new TestContext() + .withActions([category]) + .construct(); + // act + const actual = sut.getScriptsByLevel(currentLevel); + // assert + expect(actual).to.not.include(toIgnore); + } + }); + it(`${RecommendationLevel[RecommendationLevel.Standard]} filters ${RecommendationLevel[RecommendationLevel.Strict]}`, () => { + // arrange + const level = RecommendationLevel.Standard; + const expected = [ + new ScriptStub('S1').withLevel(level), + new ScriptStub('S2').withLevel(level), + ]; + const actions = [ + new CategoryStub('parent-category').withScripts( + ...expected, + new ScriptStub('S3').withLevel(RecommendationLevel.Strict), + ), + ]; + const sut = new TestContext() + .withActions(actions) + .construct(); + // act + const actual = sut.getScriptsByLevel(level); + // assert + expect(expected).to.deep.equal(actual); + }); + it(`${RecommendationLevel[RecommendationLevel.Strict]} includes ${RecommendationLevel[RecommendationLevel.Standard]}`, () => { + // arrange + const level = RecommendationLevel.Strict; + const expected = [ + new ScriptStub('S1').withLevel(RecommendationLevel.Standard), + new ScriptStub('S2').withLevel(RecommendationLevel.Strict), + ]; + const actions = [ + new CategoryStub('parent-category').withScripts(...expected), + ]; + const sut = new TestContext() + .withActions(actions) + .construct(); + // act + const actual = sut.getScriptsByLevel(level); + // assert + expect(expected).to.deep.equal(actual); + }); + describe('throws when given invalid level', () => { + new EnumRangeTestRunner((level) => { + // arrange + const sut = new TestContext() + .construct(); + // act + sut.getScriptsByLevel(level); + }) + // assert + .testOutOfRangeThrows() + .testValidValueDoesNotThrow(RecommendationLevel.Standard); + }); + }); + describe('totalScripts', () => { + it('returns total of initial scripts', () => { + // arrange + const categories = [ + new CategoryStub('category-1').withScripts( + new ScriptStub('S1').withLevel(RecommendationLevel.Standard), + ), + new CategoryStub('category-2').withScripts( + new ScriptStub('S2'), + new ScriptStub('S3').withLevel(RecommendationLevel.Strict), + ), + new CategoryStub('category-3').withCategories( + new CategoryStub('category-3-subcategory-1').withScripts(new ScriptStub('S4')), + ), + ]; + // act + const sut = new TestContext() + .withActions(categories) + .construct(); + // assert + expect(sut.totalScripts).to.equal(4); + }); + }); + describe('totalCategories', () => { + it('returns total of initial categories', () => { + // arrange + const expected = 4; + const categories = [ + new CategoryStub('category-1').withScripts(new ScriptStub('S1').withLevel(RecommendationLevel.Strict)), + new CategoryStub('category-2').withScripts(new ScriptStub('S2'), new ScriptStub('S3')), + new CategoryStub('category-3').withCategories(new CategoryStub('category-3-subcategory-1').withScripts(new ScriptStub('S4'))), + ]; + // act + const sut = new TestContext() + .withActions(categories) + .construct(); + // assert + expect(sut.totalCategories).to.equal(expected); + }); + }); + describe('os', () => { + it('sets os as expected', () => { + // arrange + const expected = OperatingSystem.macOS; + // act + const sut = new TestContext() + .withOs(expected) + .construct(); + // assert + expect(sut.os).to.deep.equal(expected); + }); + }); + describe('scriptingDefinition', () => { + it('sets scriptingDefinition as expected', () => { + // arrange + const expected = getValidScriptingDefinition(); + // act + const sut = new TestContext() + .withScripting(expected) + .construct(); + // assert + expect(sut.scripting).to.deep.equal(expected); + }); + }); + describe('getCategory', () => { + it('throws if category is not found', () => { + // arrange + const missingCategoryId = 'missing-category-id'; + const expectedError = `Missing category with ID: "${missingCategoryId}"`; + const collection = new TestContext() + .withActions([new CategoryStub(`different than ${missingCategoryId}`).withMandatoryScripts()]) + .construct(); + // act + const act = () => collection.getCategory(missingCategoryId); + // assert + expect(act).to.throw(expectedError); + }); + it('finds correct category', () => { + // arrange + const existingCategoryId = 'expected-action-category-id'; + const expectedCategory = new CategoryStub(existingCategoryId).withMandatoryScripts(); + const collection = new TestContext() + .withActions([expectedCategory]) + .construct(); + // act + const actualCategory = collection.getCategory(existingCategoryId); + // assert + expect(actualCategory).to.equal(expectedCategory); + }); + }); + describe('getScript', () => { + it('throws if script is not found', () => { + // arrange + const scriptId = 'missingScript'; + const expectedError = `Missing script: ${scriptId}`; + const collection = new TestContext() + .withActions([new CategoryStub('parent-action').withMandatoryScripts()]) + .construct(); + // act + const act = () => collection.getScript(scriptId); + // assert + expect(act).to.throw(expectedError); + }); + it('finds correct script', () => { + // arrange + const scriptId = 'existingScript'; + const expectedScript = new ScriptStub(scriptId); + const parentCategory = new CategoryStub('parent-action') + .withMandatoryScripts() + .withScript(expectedScript); + const collection = new TestContext() + .withActions([parentCategory]) + .construct(); + // act + const actualScript = collection.getScript(scriptId); + // assert + expect(actualScript).to.equal(expectedScript); + }); + }); +}); + +function getValidScriptingDefinition(): IScriptingDefinition { + return { + fileExtension: '.bat', + language: ScriptingLanguage.batchfile, + startCode: 'start', + endCode: 'end', + }; +} + +class TestContext { + private os = OperatingSystem.Windows; + + private actions: readonly Category[] = [ + new CategoryStub(`[${TestContext.name}]-action-1`).withMandatoryScripts(), + ]; + + private scriptingDefinition: IScriptingDefinition = getValidScriptingDefinition(); + + public withOs(os: OperatingSystem): this { + this.os = os; + return this; + } + + public withActions(actions: readonly Category[]): this { + this.actions = actions; + return this; + } + + public withScripting(scriptingDefinition: IScriptingDefinition): this { + this.scriptingDefinition = scriptingDefinition; + return this; + } + + public construct(): CategoryCollection { + return new CategoryCollection({ + os: this.os, + actions: this.actions, + scripting: this.scriptingDefinition, + }); + } +} diff --git a/tests/unit/domain/Collection/Validation/CompositeCategoryCollectionValidator.spec.ts b/tests/unit/domain/Collection/Validation/CompositeCategoryCollectionValidator.spec.ts new file mode 100644 index 00000000..2250c28a --- /dev/null +++ b/tests/unit/domain/Collection/Validation/CompositeCategoryCollectionValidator.spec.ts @@ -0,0 +1,170 @@ +import { describe, it, expect } from 'vitest'; +import { validateCategoryCollection } from '@/domain/Collection/Validation/CompositeCategoryCollectionValidator'; +import type { CategoryCollectionValidationContext, CategoryCollectionValidator } from '@/domain/Collection/Validation/CategoryCollectionValidator'; +import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub'; + +describe('validateCategoryCollection', () => { + it('throws error when no validators are provided', () => { + // arrange + const emptyValidators: CategoryCollectionValidator[] = []; + const expectedErrorMessage = 'No validators provided.'; + + // act + const act = () => new TestContext() + .withValidators(emptyValidators) + .runValidation(); + + // assert + expect(act).to.throw(expectedErrorMessage); + }); + + describe('validator execution', () => { + it('executes single validator', () => { + // arrange + let isCalled = false; + const singleValidator: CategoryCollectionValidator = () => { + isCalled = true; + }; + const validators = [singleValidator]; + + // act + new TestContext() + .withValidators(validators) + .runValidation(); + + // assert + expect(isCalled).to.equal(true); + }); + + it('executes multiple validators in order', () => { + // arrange + const expectedExecutionSequence: readonly string[] = [ + 'validator1Call', + 'validator2Call', + ]; + const actualExecutionSequence: string[] = []; + const validator1: CategoryCollectionValidator = () => { + actualExecutionSequence.push(expectedExecutionSequence[0]); + }; + const validator2: CategoryCollectionValidator = () => { + actualExecutionSequence.push(expectedExecutionSequence[1]); + }; + const validators = [validator1, validator2]; + + // act + new TestContext() + .withValidators(validators) + .runValidation(); + + // assert + expect(actualExecutionSequence).to.deep.equal(expectedExecutionSequence); + }); + + it('passes correct context to single validator', () => { + // arrange + const expectedContext = new CategoryCollectionValidationContextStub(); + let actualContext: CategoryCollectionValidationContext | undefined; + const validator: CategoryCollectionValidator = (context) => { + actualContext = context; + }; + const validators = [validator]; + + // act + new TestContext() + .withValidators(validators) + .withValidationContext(expectedContext) + .runValidation(); + + // assert + expect(expectedContext).to.equal(actualContext); + }); + + it('passes same context to all validators', () => { + // arrange + const expectedContext = new CategoryCollectionValidationContextStub(); + const receivedContexts = new Array(); + const contextStoringValidator: CategoryCollectionValidator = (context) => { + receivedContexts.push(context); + }; + const validators = [ + contextStoringValidator, + contextStoringValidator, + contextStoringValidator, + ]; + + // act + new TestContext() + .withValidators(validators) + .withValidationContext(expectedContext) + .runValidation(); + + // assert + expect(receivedContexts.every((c) => c === expectedContext)).to.equal(true); + }); + }); + + it('propagates error from validator', () => { + // arrange + const expectedError = 'Error from validator'; + const errorThrowingValidator: CategoryCollectionValidator = () => { + throw new Error(expectedError); + }; + const validators = [errorThrowingValidator]; + + // act + const act = () => new TestContext() + .withValidators(validators) + .runValidation(); + + // Act & Assert + expect(act).to.throw(expectedError); + }); + + it('halts execution on validator error', () => { + // arrange + const errorThrowingValidator: CategoryCollectionValidator = () => { + throw new Error('Error from validator'); + }; + let isSecondValidatorCalled = false; + const secondValidator: CategoryCollectionValidator = () => { + isSecondValidatorCalled = true; + }; + const validators = [errorThrowingValidator, secondValidator]; + + // act + try { + new TestContext() + .withValidators(validators) + .runValidation(); + } catch { /* Swallow */ } + + // Act & Assert + expect(isSecondValidatorCalled).to.equal(false); + }); +}); + +class TestContext { + private validators: readonly CategoryCollectionValidator[] = [ + () => {}, + ]; + + private validationContext + : CategoryCollectionValidationContext = new CategoryCollectionValidationContextStub(); + + public withValidators(validators: readonly CategoryCollectionValidator[]): this { + this.validators = validators; + return this; + } + + public withValidationContext(validationContext: CategoryCollectionValidationContext): this { + this.validationContext = validationContext; + return this; + } + + public runValidation(): ReturnType { + return validateCategoryCollection( + this.validationContext, + this.validators, + ); + } +} diff --git a/tests/unit/domain/Collection/Validation/Rules/EnsureKnownOperatingSystem.spec.ts b/tests/unit/domain/Collection/Validation/Rules/EnsureKnownOperatingSystem.spec.ts new file mode 100644 index 00000000..1195e383 --- /dev/null +++ b/tests/unit/domain/Collection/Validation/Rules/EnsureKnownOperatingSystem.spec.ts @@ -0,0 +1,21 @@ +import { describe } from 'vitest'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner'; +import { ensureKnownOperatingSystem } from '@/domain/Collection/Validation/Rules/EnsureKnownOperatingSystem'; +import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub'; + +describe('ensureKnownOperatingSystem', () => { + // act + const act = (os: OperatingSystem) => test(os); + // assert + new EnumRangeTestRunner(act) + .testValidValueDoesNotThrow(OperatingSystem.Android) + .testOutOfRangeThrows(); +}); + +function test(operatingSystem: OperatingSystem): +ReturnType { + const context = new CategoryCollectionValidationContextStub() + .withOperatingSystem(operatingSystem); + return ensureKnownOperatingSystem(context); +} diff --git a/tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAllRecommendationLevels.spec.ts b/tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAllRecommendationLevels.spec.ts new file mode 100644 index 00000000..17d0175a --- /dev/null +++ b/tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAllRecommendationLevels.spec.ts @@ -0,0 +1,99 @@ +import { describe, it, expect } from 'vitest'; +import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; +import type { Script } from '@/domain/Executables/Script/Script'; +import { ensurePresenceOfAllRecommendationLevels } from '@/domain/Collection/Validation/Rules/EnsurePresenceOfAllRecommendationLevels'; +import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { getEnumValues } from '@/application/Common/Enum'; +import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector'; + +describe('ensurePresenceOfAllRecommendationLevels', () => { + it('passes when all recommendation levels are present', () => { + // arrange + const scripts = getAllPossibleRecommendationLevels().map((level, index) => { + return new ScriptStub(`script-${index}`) + .withLevel(level); + }); + + // act + const act = () => test(scripts); + + // assert + expect(act).to.not.throw(); + }); + + describe('missing single level', () => { + // arrange + const recommendationLevels = getAllPossibleRecommendationLevels(); + recommendationLevels.forEach((missingLevel) => { + const expectedDisplayName = getDisplayName(missingLevel); + it(`throws an error when when "${expectedDisplayName}" is missing`, () => { + const expectedError = `Missing recommendation levels: ${expectedDisplayName}.`; + const otherLevels = recommendationLevels.filter((level) => level !== missingLevel); + const scripts = otherLevels.map( + (level, index) => new ScriptStub(`script-${index}`).withLevel(level), + ); + // act + const act = () => test(scripts); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); + + it('throws an error with multiple missing recommendation levels', () => { + // arrange + const [ + notExpectedLevelInError, + ...expectedLevelsInError + ] = getAllPossibleRecommendationLevels(); + const scripts: Script[] = [ + new ScriptStub('recommended').withLevel(notExpectedLevelInError), + ]; + + // act + const act = () => test(scripts); + + // assert + const actualErrorMessage = collectExceptionMessage(act); + expectedLevelsInError.forEach((level) => { + const expectedLevelInError = getDisplayName(level); + expect(actualErrorMessage).to.include(expectedLevelInError); + }); + expect(actualErrorMessage).to.not.include(getDisplayName(notExpectedLevelInError)); + }); + + it('throws an error when no scripts are provided', () => { + // arrange + const expectedLevelsInError = getAllPossibleRecommendationLevels() + .map((level) => getDisplayName(level)); + const scripts: Script[] = []; + + // act + const act = () => test(scripts); + + // assert + const actualErrorMessage = collectExceptionMessage(act); + expectedLevelsInError.forEach((expectedLevelInError) => { + expect(actualErrorMessage).to.include(expectedLevelInError); + }); + }); +}); + +function test(allScripts: Script[]): +ReturnType { + const context = new CategoryCollectionValidationContextStub() + .withAllScripts(allScripts); + return ensurePresenceOfAllRecommendationLevels(context); +} + +function getAllPossibleRecommendationLevels(): readonly (RecommendationLevel | undefined)[] { + return [ + ...getEnumValues(RecommendationLevel), + undefined, + ]; +} + +function getDisplayName(level: RecommendationLevel | undefined): string { + return level === undefined ? 'None' : RecommendationLevel[level]; +} diff --git a/tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneCategory.spec.ts b/tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneCategory.spec.ts new file mode 100644 index 00000000..077e4703 --- /dev/null +++ b/tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneCategory.spec.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub'; +import type { Category } from '@/domain/Executables/Category/Category'; +import { ensurePresenceOfAtLeastOneCategory } from '@/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneCategory'; +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; + +describe('ensurePresenceOfAtLeastOneCategory', () => { + it('throws an error when no categories are present', () => { + // arrange + const expectedErrorMessage = 'Collection must have at least one category'; + const categories: Category[] = []; + + // act + const act = () => test(categories); + + // assert + expect(act).to.throw(expectedErrorMessage); + }); + + it('does not throw an error when at least one category is present', () => { + // arrange + const categories: Category[] = [ + new CategoryStub('existing-category'), + ]; + + // act + const act = () => test(categories); + + // assert + expect(act).not.to.throw(); + }); +}); + +function test(allCategories: readonly Category[]): +ReturnType { + const context = new CategoryCollectionValidationContextStub() + .withAllCategories(allCategories); + return ensurePresenceOfAtLeastOneCategory(context); +} diff --git a/tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneScript.spec.ts b/tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneScript.spec.ts new file mode 100644 index 00000000..5f893e76 --- /dev/null +++ b/tests/unit/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneScript.spec.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub'; +import type { Script } from '@/domain/Executables/Script/Script'; +import { ensurePresenceOfAtLeastOneScript } from '@/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneScript'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; + +describe('ensurePresenceOfAtLeastOneScript', () => { + it('throws an error when no scripts are present', () => { + // arrange + const expectedErrorMessage = 'Collection must have at least one script'; + const scripts: Script[] = []; + + // act + const act = () => test(scripts); + + // assert + expect(act).to.throw(expectedErrorMessage); + }); + + it('does not throw an error when at least one category is present', () => { + // arrange + const scripts: Script[] = [ + new ScriptStub('existing-script'), + ]; + + // act + const act = () => test(scripts); + + // assert + expect(act).not.to.throw(); + }); +}); + +function test(allScripts: readonly Script[]): +ReturnType { + const context = new CategoryCollectionValidationContextStub() + .withAllScripts(allScripts); + return ensurePresenceOfAtLeastOneScript(context); +} diff --git a/tests/unit/domain/Collection/Validation/Rules/EnsureUniqueIdsAcrossExecutables.spec.ts b/tests/unit/domain/Collection/Validation/Rules/EnsureUniqueIdsAcrossExecutables.spec.ts new file mode 100644 index 00000000..6069e754 --- /dev/null +++ b/tests/unit/domain/Collection/Validation/Rules/EnsureUniqueIdsAcrossExecutables.spec.ts @@ -0,0 +1,146 @@ +import { describe, it, expect } from 'vitest'; +import { ensureUniqueIdsAcrossExecutables } from '@/domain/Collection/Validation/Rules/EnsureUniqueIdsAcrossExecutables'; +import type { Category } from '@/domain/Executables/Category/Category'; +import type { Script } from '@/domain/Executables/Script/Script'; +import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub'; +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; + +describe('ensureUniqueIdsAcrossExecutables', () => { + it('does not throw an error when all IDs are unique', () => { + // arrange + const testData: TestData = { + categories: [ + new CategoryStub('category1'), + new CategoryStub('category2'), + ], + scripts: [ + new ScriptStub('script1'), + new ScriptStub('script2'), + ], + }; + + // act + const act = () => test(testData); + + // assert + expect(act).to.not.throw(); + }); + + it('throws an error when duplicate IDs are found across categories and scripts', () => { + // arrange + const duplicateId: ExecutableId = 'duplicate'; + const expectedErrorMessage = `Duplicate executable IDs found: "${duplicateId}"`; + const testData: TestData = { + categories: [ + new CategoryStub(duplicateId), + new CategoryStub('category2'), + ], + scripts: [ + new ScriptStub(duplicateId), + new ScriptStub('script2'), + ], + }; + + // act + const act = () => test(testData); + + // assert + expect(act).to.throw(expectedErrorMessage); + }); + + it('throws an error when duplicate IDs are found within categories', () => { + // arrange + const duplicateId: ExecutableId = 'duplicate'; + const expectedErrorMessage = `Duplicate executable IDs found: "${duplicateId}"`; + const testData: TestData = { + categories: [ + new CategoryStub(duplicateId), + new CategoryStub(duplicateId), + ], + scripts: [ + new ScriptStub('script1'), + new ScriptStub('script2'), + ], + }; + + // act + const act = () => test(testData); + + // assert + expect(act).to.throw(expectedErrorMessage); + }); + + it('throws an error when duplicate IDs are found within scripts', () => { + // arrange + const duplicateId: ExecutableId = 'duplicate'; + const expectedErrorMessage = `Duplicate executable IDs found: "${duplicateId}"`; + const testData: TestData = { + categories: [ + new CategoryStub('category1'), + new CategoryStub('category2'), + ], + scripts: [ + new ScriptStub(duplicateId), + new ScriptStub(duplicateId), + ], + }; + + // act + const act = () => test(testData); + + // assert + expect(act).to.throw(expectedErrorMessage); + }); + + it('throws an error with multiple duplicate IDs', () => { + // arrange + const duplicateId1: ExecutableId = 'duplicate-1'; + const duplicateId2: ExecutableId = 'duplicate-2'; + const expectedErrorMessage = `Duplicate executable IDs found: "${duplicateId1}", "${duplicateId2}"`; + const testData: TestData = { + categories: [ + new CategoryStub(duplicateId1), + new CategoryStub(duplicateId2), + ], + scripts: [ + new ScriptStub(duplicateId1), + new ScriptStub(duplicateId2), + ], + }; + + // act + const act = () => test(testData); + + // assert + expect(act).to.throw(expectedErrorMessage); + }); + + it('handles empty categories and scripts arrays', () => { + // arrange + const testData: TestData = { + categories: [], + scripts: [], + }; + + // act + const act = () => test(testData); + + // assert + expect(act).to.not.throw(); + }); +}); + +interface TestData { + readonly categories: readonly Category[]; + readonly scripts: readonly Script[]; +} + +function test(testData: TestData): +ReturnType { + const context = new CategoryCollectionValidationContextStub() + .withAllCategories(testData.categories) + .withAllScripts(testData.scripts); + return ensureUniqueIdsAcrossExecutables(context); +} diff --git a/tests/unit/domain/Executables/Category/CategoryFactory.spec.ts b/tests/unit/domain/Executables/Category/CategoryFactory.spec.ts new file mode 100644 index 00000000..ddd16217 --- /dev/null +++ b/tests/unit/domain/Executables/Category/CategoryFactory.spec.ts @@ -0,0 +1,316 @@ +import { describe, it, expect } from 'vitest'; +import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; +import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; +import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import type { Category } from '@/domain/Executables/Category/Category'; +import type { Script } from '@/domain/Executables/Script/Script'; +import { createCategory } from '@/domain/Executables/Category/CategoryFactory'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; + +describe('CategoryFactory', () => { + describe('createCategory', () => { + describe('id', () => { + it('assigns id correctly', () => { + // arrange + const expectedId: ExecutableId = 'expected category id'; + // act + const category = new TestContext() + .withExecutableId(expectedId) + .build(); + // assert + const actualId = category.executableId; + expect(actualId).to.equal(expectedId); + }); + describe('throws error if id is absent', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing ID'; + const id = absentValue; + // act + const construct = () => new TestContext() + .withExecutableId(id) + .build(); + // assert + expect(construct).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + }); + describe('name', () => { + it('assigns name correctly', () => { + // arrange + const expectedName = 'expected category name'; + // act + const category = new TestContext() + .withName(expectedName) + .build(); + // assert + const actualName = category.name; + expect(actualName).to.equal(expectedName); + }); + describe('throws error if name is absent', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing name'; + const name = absentValue; + // act + const construct = () => new TestContext() + .withName(name) + .build(); + // assert + expect(construct).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + }); + describe('docs', () => { + it('assigns docs correctly', () => { + // arrange + const expectedDocs = ['expected', 'docs']; + // act + const category = new TestContext() + .withDocs(expectedDocs) + .build(); + // assert + const actualDocs = category.docs; + expect(actualDocs).to.equal(expectedDocs); + }); + }); + describe('children', () => { + it('assigns scripts correctly', () => { + // arrange + const expectedScripts = [ + new ScriptStub('expected-script-1'), + new ScriptStub('expected-script-2'), + ]; + // act + const category = new TestContext() + .withScripts(expectedScripts) + .build(); + // assert + const actualScripts = category.scripts; + expect(actualScripts).to.equal(expectedScripts); + }); + it('assigns categories correctly', () => { + // arrange + const expectedCategories = [ + new CategoryStub('expected-subcategory-1'), + new CategoryStub('expected-subcategory-2'), + ]; + // act + const category = new TestContext() + .withSubcategories(expectedCategories) + .build(); + // assert + const actualCategories = category.subcategories; + expect(actualCategories).to.equal(expectedCategories); + }); + it('throws error if no children are present', () => { + // arrange + const expectedError = 'A category must have at least one sub-category or script'; + const scriptChildren: readonly Script[] = []; + const categoryChildren: readonly Category[] = []; + // act + const construct = () => new TestContext() + .withSubcategories(categoryChildren) + .withScripts(scriptChildren) + .build(); + // assert + expect(construct).to.throw(expectedError); + }); + }); + describe('getAllScriptsRecursively', () => { + it('retrieves direct child scripts', () => { + // arrange + const expectedScripts: readonly Script[] = [ + new ScriptStub('expected-script-1'), + new ScriptStub('expected-script-2'), + ]; + const category = new TestContext() + .withScripts(expectedScripts) + .build(); + // act + const actual = category.getAllScriptsRecursively(); + // assert + expect(actual).to.have.deep.members(expectedScripts); + }); + it('retrieves scripts from direct child categories', () => { + // arrange + const expectedScriptIds: readonly string[] = [ + '1', '2', '3', '4', + ]; + const subcategories: readonly Category[] = [ + new CategoryStub('subcategory-1').withScriptIds('1', '2'), + new CategoryStub('subcategory-2').withScriptIds('3', '4'), + ]; + const category = new TestContext() + .withScripts([]) + .withSubcategories(subcategories) + .build(); + // act + const actualIds = category + .getAllScriptsRecursively() + .map((s) => s.executableId); + // assert + expect(actualIds).to.have.deep.members(expectedScriptIds); + }); + it('retrieves scripts from both direct children and child categories', () => { + // arrange + const expectedScriptIds: readonly string[] = [ + '1', '2', '3', '4', '5', '6', + ]; + const subcategories: readonly Category[] = [ + new CategoryStub('subcategory-1').withScriptIds('1', '2'), + new CategoryStub('subcategory-2').withScriptIds('3', '4'), + ]; + const scripts: readonly Script[] = [ + new ScriptStub('5'), + new ScriptStub('6'), + ]; + const category = new TestContext() + .withSubcategories(subcategories) + .withScripts(scripts) + .build(); + // act + const actualIds = category + .getAllScriptsRecursively() + .map((s) => s.executableId); + // assert + expect(actualIds).to.have.deep.members(expectedScriptIds); + }); + it('retrieves scripts from nested categories recursively', () => { + // arrange + const expectedScriptIds: readonly string[] = [ + '1', '2', '3', '4', '5', '6', + ]; + const subcategories: readonly Category[] = [ + new CategoryStub('subcategory-1') + .withScriptIds('1', '2') + .withCategory( + new CategoryStub('subcategory-1-subcategory-1') + .withScriptIds('3', '4'), + ), + new CategoryStub('subcategory-2') + .withCategories( + new CategoryStub('subcategory-2-subcategory-1') + .withScriptIds('5') + .withCategory( + new CategoryStub('subcategory-2-subcategory-1-subcategory-1') + .withCategory( + new CategoryStub('subcategory-2-subcategory-1-subcategory-1-subcategory-1') + .withScriptIds('6'), + ), + ), + ), + ]; + // assert + const category = new TestContext() + .withScripts([]) + .withSubcategories(subcategories) + .build(); + // act + const actualIds = category + .getAllScriptsRecursively() + .map((s) => s.executableId); + // assert + expect(actualIds).to.have.deep.members(expectedScriptIds); + }); + }); + describe('includes', () => { + it('returns false for scripts not included', () => { + // assert + const expectedResult = false; + const script = new ScriptStub('3'); + const childCategory = new CategoryStub('subcategory') + .withScriptIds('1', '2'); + const category = new TestContext() + .withSubcategories([childCategory]) + .build(); + // act + const actual = category.includes(script); + // assert + expect(actual).to.equal(expectedResult); + }); + it('returns true for scripts directly included', () => { + // assert + const expectedResult = true; + const script = new ScriptStub('3'); + const childCategory = new CategoryStub('subcategory') + .withScript(script) + .withScriptIds('non-related'); + const category = new TestContext() + .withSubcategories([childCategory]) + .build(); + // act + const actual = category.includes(script); + // assert + expect(actual).to.equal(expectedResult); + }); + it('returns true for scripts included in nested categories', () => { + // assert + const expectedResult = true; + const script = new ScriptStub('3'); + const childCategory = new CategoryStub('subcategory') + .withScriptIds('non-related') + .withCategory( + new CategoryStub('nested-subcategory') + .withScript(script), + ); + const category = new TestContext() + .withSubcategories([childCategory]) + .build(); + // act + const actual = category.includes(script); + // assert + expect(actual).to.equal(expectedResult); + }); + }); + }); +}); + +class TestContext { + private executableId: ExecutableId = `[${TestContext.name}] test category`; + + private name = 'test-category'; + + private docs: ReadonlyArray = []; + + private subcategories: ReadonlyArray = []; + + private scripts: ReadonlyArray