Compare commits

..

63 Commits

Author SHA1 Message Date
undergroundwires
d328f08952 Fix incorrect URL rendering in documentation texts
This commit addresses incorrect URL rendering within documentation text
by improving auto-linkified URL labels, handling `+` symbols as spaces,
enhancing readability of encoded path segments and manually updating
some of the documetation.

Key improvements:

- Parse `+` as whitespace in URLs for accurate script labeling.
- Interpret multiple whitespaces as single for robustness.
- Decode path segments for clearer links.
- Refactor markdown renderer.
- Expand unit tests for comprehensive coverage.

Documentation has been updated to fix inline URL references and improve
linkification across several scripts, ensuring more readable and
user-friendly content.

Affected files and documentation sections have been adjusted
accordingly, including script and category names for consistency and
clarity.

Some of the script/category documentation changing fixing URL rendering
includes:

- 'Disable sending information to Customer Experience Improvement
  Program':
  - Fix reference URLs being inlined.
- 'Disable "Secure boot" button in "Windows Security"':
  - Fix rendering issue due to auto-linkification of `markdown-it`.
- 'Clear Internet Explorer DOMStore':
  - Fix rendering issue due to auto-linkification of `markdown-it`.
- 'Disable "Windows Defender Firewall" service':
  - Fix rendering issue due to auto-linkification of `markdown-it`.
  - Convert YAML comments to markdown comments visible by users.
  - Add breaking behavior to script name, changing script name to.
- 'Disable Microsoft Defender Firewall services and drivers':
  - Remove information about breaking behavior to avoid duplication and
    be consistent with the documentation of the rest of the collections.
- Use consistent styling for warning texts starting with `Caution:`.
- Rename 'Remove extensions' category to 'Remove extension apps' for
  consistency with names of its sibling categories.
2023-11-27 05:17:58 +01:00
undergroundwires
bcad357017 linux: fix Firefox settings not reverting #282
Improve the revert process for Firefox settings by extending
modifications to also include `prefs.js`.

- Validate profile directories similarly to execution script.
- Check and warn if Firefox is running during revert to prevent
  `prefs.js` from being overriden.
- Clarify output messages for execution and revert scripts.
- Add flowchart diagram for visual documentation.
- Improve documentation for consistency and precision.
- Update `.gitignore` to account for temporary draw.io files.
2023-11-26 01:20:21 +01:00
undergroundwires
9845a7cd68 Fix rendering of inline code blocks for docs
Styling of codeblocks:

- Uniform margins as other documentation elements.
- Add small margin for inline code-blocks.
- Use different background color for inline code-blocks.
- Introduce `inline-code` and `code-block` mixins for clarity in
  styling.

Overflowing of codeblocks:

- Improve flex layout of the tree component to be handle overflowing
  content and providing maximum available width. To be able to correctly
  provide maximum available width in card content, card expansion layout
  is changed so both close button and the content gets their full width.
- Other refactorings to support this:
  - Introduce separate Vue component for checkboxes of nodes for better
    separation of concerns and improved maintainability.
  - Refactor `LeafTreeNode` to make it simpler, separating layout concerns
    from other styling.
  - `ScriptsTree.vue`: Prefer `<div>`s instead of `<span>`s as they
    represent large content.
  - Remove unnecessary `<div>`s and use `<template>`s to reduce HTML
    complexity.
  - Update script documentation to not include unnecessary left padding
    on script code blocks.
  - Refactor SCSS variable names in `DocumentationText.vue` for clarity.
2023-11-25 11:03:33 +01:00
undergroundwires
7c632f7388 win: fix system app removal affecting updates #287
This commit fixes an issue where removing systems apps could disrupt
Windows Cumulative updates as reported in #287.

The fix involves removing the `EndOfLife` registry key after the app is
removed. Keeping the key at
`HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore\EndOfLife`
was identified as the cause for update failures in #287.

This commit also refactors the registry key creation/removal logic to be
owned by separate functions for easier readability and reusability.
2023-11-24 14:52:52 +01:00
undergroundwires
1442f62633 Fix spacing in documentation for readability
This commit improves the readability of the script/category
documentations by refining the vertical and horizontal spacing. The
adjustments aim to create more visually consistent layout.

Styling changes include:

- Add more and consistent spacing between text parts (such as lists,
  tables, paragraphs etc.).
- Remove top/bottom spacing at the start and end of the text.
- Add more horizontal spacing (padding on left) for lists.
- Improve blockquote styling with a border and similar spacing as other
  parts.
2023-11-23 07:39:11 +01:00
undergroundwires
7f7a84e3ba win: discourage IntelliCode disabling #267, #286
This commit fixes issues #267 and #286 where users reported that
disabling IntelliCode data collection causes Visual Studio 2022 to hang
or become unresponsive.

The script has been updated to remove its recommendation status and
include a warning about these issues. As Microsoft did not respond to
inconsistencies with the official documentation in
MicrosoftDocs/intellicode#510, this commit prevents further disruptions
for privacy.sexy users.
2023-11-22 11:59:35 +01:00
undergroundwires
dee3279f85 win: fix persistent update disabling /w tasks #272
This patch improves the existing functionality for disabling Windows
Updates. It ensures that the disabling of automatic updates is more
persistent, addressing previous shortcomings.

This commit introduces the "Disable Windows Update scheduled tasks"
category, enabling users to persistently turn off automatic background
updates.

Supporting changes include:

- Improve `DisableScheduledTask`:
  - Add the ability to elevate privileges.
  - Add the ability to disable tasks upon script reversion to match the
    correct default operating system state.
  - Fix warning output not being correctly formatted upon script
    reversion.
- Add the ability to disable tasks upon script reversion in
  `DisableScheduledTask` to match the correct default operating system
  state.
- Add a comment to clarify the rationale behind not disabling certain
  Windows services.
- Ensure consistent casing (all uppercase) for Windows environment
  variables in documentation.
- Ensure consistent and right casing of Windows folder names in scripts
  and their documentation.
2023-11-21 05:07:43 +01:00
undergroundwires
094dbb01b8 win: fix and improve Store app categorization #190
- Remove incorrect categories and flatten their children.
- Simplify user interface by removing "installed" and "provisioned" app
  categories, listing the apps directly.
- Indent comments to be easily collapse parent category in IDE.
- Improve some of the existing documentation.
2023-11-20 11:26:14 +01:00
undergroundwires
e299d40fa1 Fix layout jumps/shifts and overflow on modals
This commit improves the user interface in modal display.

- Prevent layout shifts caused by background scrollbars when modals are
  active.
- Fix unintended overflow of modals on small screens, preventing parts
  of the modal from being cut off on the right side.
- Refactor DOM manipulation, enhancing modularity, reusability,
  extensibility, and separation of concerns.
- Centralize viewport test scenarios for different sizes in a single
  definition for E2E tests.
2023-11-19 23:51:25 +01:00
undergroundwires
cb42f11b97 Fix code highlighting and optimize category select
This commit introduces a batched debounce mechanism for managing user
selection state changes. It effectively reduces unnecessary processing
during rapid script checking, preventing multiple triggers for code
compilation and UI rendering.

Key improvements include:

- Enhanced performance, especially noticeable when selecting large
  categories. This update resolves minor UI freezes experienced when
  selecting categories with numerous scripts.
- Correction of a bug where the code area only highlighted the last
  selected script when multiple scripts were chosen.

Other changes include:

- Timing functions:
  - Create a `Timing` folder for `throttle` and the new
    `batchedDebounce` functions.
  - Move these functions to the application layer from the presentation
    layer, reflecting their application-wide use.
  - Refactor existing code for improved clarity, naming consistency, and
    adherence to new naming conventions.
  - Add missing unit tests.
- `UserSelection`:
  - State modifications in `UserSelection` now utilize a singular object
    inspired by the CQRS pattern, enabling batch updates and flexible
    change configurations, thereby simplifying change management.
- Remove the `I` prefix from related interfaces to align with new coding
  standards.
- Refactor related code for better testability in isolation with
  dependency injection.
- Repository:
  - Move repository abstractions to the application layer.
  - Improve repository abstraction to combine `ReadonlyRepository` and
    `MutableRepository` interfaces.
- E2E testing:
  - Introduce E2E tests to validate the correct batch selection
    behavior.
  - Add a specialized data attribute in `TheCodeArea.vue` for improved
    testability.
  - Reorganize shared Cypress functions for a more idiomatic Cypress
    approach.
  - Improve test documentation with related information.
- `SelectedScript`:
  - Create an abstraction for simplified testability.
  - Introduce `SelectedScriptStub` in tests as a substitute for the
    actual object.
2023-11-18 22:23:27 +01:00
undergroundwires
4531645b4c Refactor to Vue 3 recommended ESLint rules
These updates ensure better adherence to Vue 3 standards and improve
overall code quality and readability.

- Update ESLint configuration from Vue 2.x to Vue 3 rules.
- Switch from "essential" to strictest "recommended" ESLint ruleset.
- Adjust ESLint script to treat warnings as errors by using
  `--max-warnings=0` flag. This enforces stricter code quality controls
  provided by Vue 3 rules.
2023-11-17 13:57:13 +01:00
undergroundwires
bf3426f91b Fix card list UI layout shifts (jumps) on load
This commit fixes layout shifts that occur on card list part of the page
when the page is initially loaded.

- Resolve issue where card list starts with minimal width, leading
  to jumps in UI until correct width is calculated on medium and
  big screens.
- Dispose of existing `ResizeObserver` properly before creating a new
  one. This prevents leaks and incorrect width calculations if
  `containerElement` changes.
- Throttle resize events to minimize width/height calculation changes,
  enhancing performance and reducing the chances for layout shifts.

Supporting CI/CD improvements:

- Enable artifact upload in CI/CD even if E2E tests fail.
- Distinguish uploaded artifacts by operating system for clarity.
2023-11-16 16:06:33 +01:00
undergroundwires
3864f04218 win: improve disabling of scheduled tasks
This commit:

- Reduces false-positive error messages when disabling scheduled tasks.
  E.g., `ERROR: The specified task name ... does not exist in the system.`
- Centralizes and unifies the logic for disabling scheduled tasks.
- Adds additional documentation, including the existence status of tasks
  on default installations.
- Updates and improves the scripts that disable scheduled tasks.
- Improves consistency of headers in documentation text by removing the
  top margin introduces by headers.

Introduces `DisableScheduledTask` templating function:

- It provides a unified way of disabling scheduled tasks.
- It displays user-friendly messages if a task cannot be found.
- It can now handle multiple tasks found matching a pattern.
- The script now exits with the correct error code.
- It skips enable/disable actions if it's not necessary.

Improve existing scripts:

- 'Disable Google update services':
  - Rename to 'Disable Google background automatic updates'.
  - Add missing scheduled tasks observed in newer versions of Chrome.
  - Change the recommendation for disabling certain tasks to `Strict`,
    as they may interfere with Google Credential Provider as
    side-effect.
  - Separate into more categories/scripts for better granularity and
    documentation.
- 'Disable Adobe Acrobat update services':
  - Rename to 'Disable Adobe background automatic updates'.
  - Separate into more categories/scripts for enhanced granularity and
    documentation.
  - Remove end-of-life `Adobe Flash Player Updater` scheduled task and
    `adobeflashplayerupdatesvc`.
- 'Disable Dropbox automatic update services':
  - Rename to 'Disable Dropbox background automatic updates'.
  - Seperate into more categories/scripts.
- 'Disable Webcam Telemetry (`devicecensus.exe`)':
  - Rename to 'Disable census data collection'.
  - Add the disabling of the "Device User" task under it.
- 'Disable `devicecensus.exe` (telemetry) process':
  - Rename to 'Disable device and configuration data collection tool'.
- 'Disable Nvidia telemetry services':
  - Rename to 'Disable Nvidia telemetry scheduled tasks'.
  - Converted into a category for better granularity.
  - Improve documentation.
- 'Disable Defender tasks':
  - Rename to 'Disable Defender scheduled tasks'.
- 'Disable "Windows Defender ExploitGuard" task':
  - Rename to 'Disable "ExploitGuard MDM policy Refresh" task'.
- 'Remove Nvidia telemetry tasks':
  - Rename to 'Remove Nvidia telemetry packages', as "tasks" often
    refers to scheduled tasks.
- 'Disable Microsoft Office Subscription Heartbeat'
  - Rename to 'Disable "Microsoft Office Subscription Heartbeat" task'.
  - Remove disabling of the undocumented `Office 16 Subscription
    Heartbeat` task.
- 'Disable OneDrive scheduled tasks':
  - Improve documentation.
  - Add disabling of 'OneDrive Per-Machine Standalone Update' task.
- 'Disable Customer Experience Improvement Program'
  - Rename to 'Disable "Customer Experience Improvement Program"
    scheduled tasks' for clarity.
2023-11-15 21:30:42 +01:00
undergroundwires
e541a35e86 Fix mobile layout overflow caused by tooltips
This commit fixes an issue where tooltips create unwanted horizontal
overflow on mobile devices.

An overlay has been added to contain the tooltip within the viewport,
ensuring it doesn't disrupt the page layout.

The changes include adjustments to CSS visibility and pointer event
handling for the tooltip container and its children.

Changes:

- Introduce an overlay that spans the entire viewport for the tooltip
  container.
- Add CSS rules to ensure the tooltip and its children maintain correct
  pointer events and overflow behavior.
- Add a Cypress end-to-end test that verifies the absence of the
  unintended horizontal overflow on small screens.
- Uploads videos/screenshots as artifacts during CI/CD to provide easier
  troubleshooting. This change is supported by creating
  `cypress-dirs.json` to be able to share directory information with
  CI/CD runners and cypress configuration file.
2023-11-14 13:46:53 +01:00
undergroundwires
bd383ed273 Fix icon tooltip alignment on instructions modal
This commit fixes a UI misalignment issue for toolips in the download
instructions modal.

The existing margin on icons caused tooltips to be misaligned upon hover
or touch.

Changes include:

- Introduce wrapper elements to encapsulate the margin, ensuring
  tooltips align with the corresponding icons.
- Extract the information incon into a separate Vue component to adhere
  to the single-responsibility principle and improve maintainability.
- Remove redundant newline at the end of 'Open terminal' tooltip to
  reduce the visual clutter.
2023-11-13 19:02:14 +01:00
undergroundwires
949fac1a7c Refactor to enforce strictNullChecks
This commit applies `strictNullChecks` to the entire codebase to improve
maintainability and type safety. Key changes include:

- Remove some explicit null-checks where unnecessary.
- Add necessary null-checks.
- Refactor static factory functions for a more functional approach.
- Improve some test names and contexts for better debugging.
- Add unit tests for any additional logic introduced.
- Refactor `createPositionFromRegexFullMatch` to its own function as the
  logic is reused.
- Prefer `find` prefix on functions that may return `undefined` and
  `get` prefix for those that always return a value.
2023-11-12 22:54:00 +01:00
undergroundwires
7ab16ecccb Refactor watch sources for reliability
This commit changes `WatchSource` signatures into `Readonly<Ref>`s.

It provides two important benefits:

1. Eliminates the possibility of `undefined` states, that's result of
   using `WatchSource`s. This previously required additional null checks.
   By using `Readonly<Ref>`, the state handling becomes simpler and less
   susceptible to null errors.
2. Optimizes performance by using references:
   - Avoids the reactive layer of `computed` references when not needed.
   - The `watch` syntax, such as `watch(() => ref.value)`, can introduce
     side effects. For example, it does not account for `triggerRef` in
     scenarios where the value remains unchanged, preventing the watcher
     from running (vuejs/core#9579).
2023-11-11 13:55:21 +01:00
undergroundwires
58cd551a30 Refactor user selection state handling using hook
This commit introduces `useUserSelectionState` compositional hook. it
centralizes and allows reusing the logic for tracking and mutation user
selection state across the application.

The change aims to increase code reusability, simplify the code, improve
testability, and adhere to the single responsibility principle. It makes
the code more reliable against race conditions and removes the need for
tracking deep changes.

Other supporting changes:

- Introduce `CardStateIndicator` component for displaying the card state
  indicator icon, improving the testability and separation of concerns.
- Refactor `SelectionTypeHandler` to use functional code with more clear
  interfaces to simplify the code. It reduces complexity, increases
  maintainability and increase readability by explicitly separating
  mutating functions.
- Add new unit tests and extend improving ones to cover the new logic
  introduced in this commit. Remove the need to mount a wrapper
  component to simplify and optimize some tests, using parameter
  injection to inject dependencies intead.
2023-11-10 13:16:53 +01:00
undergroundwires
7770a9b521 Refactor DI for simplicity and type safety
This commit improves the dependency injection mechanism by introducing a
custom `injectKey` function.

Key improvements are:

- Enforced type consistency during dependency registration and
  instantiation.
- Simplified injection process, abstracting away the complexity with a
  uniform API, regardless of the dependency's lifetime.
- Eliminated the possibility of `undefined` returns during dependency
  injection, promoting fail-fast behavior.
- Removed the necessity for type casting to `symbol` for injection keys
  in unit tests by using existing types.
- Consalidated imports, combining keys and injection functions in one
  `import` statement.
2023-11-09 13:17:38 +01:00
undergroundwires
aab0f7ea46 Remove duplicated index.html file
This commit removes the redundant `index.html` file from the
`src/presentation/public` directory. This file was initially created
there with Vue CLI before migration to Vite. The existence of this file
is now unnecessary as Vite requires `index.html` to be at the project
root.

The deletion of this duplicate file simplifies the project and
eliminates potential confusion regarding the entry point of the
application.

Changes:

- Update `docs/presentation.md` to clarify the location of `index.html`.
- Remove the `src/presentation/public/index.html` file, which was
  duplicate of the project root `index.html`.

These changes ensure compliance with Vite's configuration and project
structure clarity.
2023-11-08 17:06:53 +01:00
undergroundwires-bot
ea41f4f503 ⬆️ bump everywhere to 0.12.7 2023-11-07 01:06:52 +00:00
undergroundwires
af7219f6e1 Fix tree node check states not being updated
This commit fixes a bug where the check states of tree nodes were not
correctly updated upon selecting predefined groups like "Standard",
"Strict", "None" and "All".

It resolves the issue by manually triggering of updates for mutated
array containing selected scripts.

It enhances test coverage to prevent regression and verify the correct
behavior of state updates.

This bug was introduced in commit
4995e49c46, which optimized reactivity by
removing deep state tracking.
2023-11-07 01:14:38 +01:00
undergroundwires
8ccaec7af6 Fix unresponsive copy button on instructions modal
This commit fixes the bug where the "Copy" button does not copy when
clicked on download instructions modal (on Linux and macOS).

This commit also introduces several improvements to the UI components
related to copy action and their interaction with the clipboard feature.

It adds more tests to avoid regression of the bugs and improves
maintainability, testability and adherence to Vue's reactive principles.

Changes include:

- Fix non-responsive copy button in the download instructions modal by
  triggering a `click` event in `AppIcon.vue`.
- Improve `TheCodeButtons.vue`:
  - Remove redundant `getCurrentCode` function.
  - Separate components for each button for better separation of
    concerns and higher testability.
  - Use the `gap` property in the flexbox layout, replacing the less
    explicit sibling combinator approach.
- Add `useClipboard` compositional hook for more idiomatic Vue approach
  to interacting with the clipboard.
- Add `useCurrentCode` compositional hook to handle current code state
  more effectively with unified logic.
- Abstract clipboard operations to an interface to isolate
  responsibilities.
- Switch clipboard implementation to the `navigator.clipboard` API,
  moving away from the deprecated `document.execCommand`.
- Move clipboard logic to the presentation layer to conform to
  separation of concerns and domain-driven design principles.
- Improve `IconButton.vue` component to increase reusability with
  consistent sizing.
2023-11-06 21:55:43 +01:00
undergroundwires
b2ffc90da7 Add winget download instructions 2023-11-05 17:05:17 +01:00
undergroundwires-bot
72e4d0b896 ⬆️ bump everywhere to 0.12.6 2023-11-04 12:14:46 +00:00
undergroundwires
5bb13e34f8 win: fix store revert for multiple installs #260
This commit improves the revert script for store apps to handle
scenarios where `Get-AppxPackage` returns multiple packages. Instead of
relying on a single package result, the script now iterates over all
found packages and attempts installation using the `AppxManifest.xml`
for each. This ensures that even if multiple versions or instances of a
package are found, the script will robustly handle and attempt to install
each one until successful.

Other changes:

- Add better message with suggestion if the revert code fails, as
  discussed in #270.
- Improve robustness of finding manifest path by using `Join-Path`
  instead of basic string concatenation. This resolves wrong paths being
  built due to missing `\` in file path.
- Add check for null or empty `InstallLocation` before accessing
  manifest path. It prevents errors when accessing `AppxManifest.xml`,
  enhancing script robustness and reliability.
- Improve error handling in manifest file existence check with try-catch
  block to catch and log exceptions, ensuring uninterrupted script
  execution in edge cases such as when the script lacks access to read
  the directory.
- Add verification of package installation before attempting to install
  the package for increased robustness.
- Add documentation for revertCode.
2023-11-03 15:24:15 +01:00
undergroundwires
0466b86f10 win, linux: unify & improve Firefox clean-up #273
This commit unifies some of the logic, documentation and naming for
Firefox clean-up with improvements on both Linux and Windows platforms.

Windows:

- 'Clear browsing history and cache':
  - Not recommend.
  - Align script name and logic with Linux implementation.
  - New documentation and not including the script in recommendation
    provides safety against unintended data loss as discussed in #273.
- 'Clear Firefox user profiles, settings, and data':
  - Rename to 'Clear all Firefox user information and preferences' for
    improved clarity.
  - Add more documentation.

Linux:

- Replace `DeleteFromFirefoxProfiles` with
  `DeleteFilesFromFirefoxProfiles`.
- Migrate implementation to Python:
  - Add more user-friendly outputs.
  - Exclude removing directory itself for additional safety.

Both Linux and Windows:

- Improve documentation for:
  - 'Clear Firefox user profiles, settings, and data'
  - 'Clear Firefox history'
2023-11-02 13:18:54 +01:00
undergroundwires
ca81f68ff1 Migrate to Vue 3.0 #230
- Migrate from "Vue 2.X" to "Vue 3.X"
- Migrate from "Vue Test Utils v1" to "Vue Test Utils v2"

Changes in detail:

- Change `inserted` to `mounted`.
- Change `::v-deep` to `:deep`.
- Change to Vue 3.0 `v-modal` syntax.
- Remove old Vue 2.0 transition name, keep the ones for Vue 3.0.
- Use new global mounting API `createApp`.
- Change `destroy` to `unmount`.
- Bootstrapping:
  - Move `provide`s for global dependencies to a bootsrapper from
    `App.vue`.
  - Remove `productionTip` setting (not in Vue 3).
  - Change `IVueBootstrapper` for simplicity and Vue 3 compatible API.
  - Add missing tests.
- Remove `.text` access on `VNode` as it's now internal API of Vue.
- Import `CSSProperties` from `vue` instead of `jsx` package.
- Shims:
  - Remove unused `shims-tsx.d.ts`.
  - Remove `shims-vue.d.ts` that's missing in quickstart template.
- Unit tests:
  - Remove old typing workaround for mounting components.
  - Rename `propsData` to `props`.
  - Remove unneeded `any` cast workarounds.
  - Move stubs and `provide`s under `global` object.

Other changes:

- Add `dmg-license` dependency explicitly due to failing electron builds
  on macOS (electron-userland/electron-builder#6520,
  electron-userland/electron-builder#6489). This was a side-effect of
  updating dependencies for this commit.
2023-11-01 13:39:39 +01:00
undergroundwires
4995e49c46 Improve UI performance by optimizing reactivity
- Replace `ref`s with `shallowRef` when deep reactivity is not needed.
- Replace `readonly`s with `shallowReadonly` where the goal is to only
  prevent `.value` mutation.
- Remove redundant `ref` in `SizeObserver.vue`.
- Remove redundant nested `ref` in `TooltipWrapper.vue`.
- Remove redundant `events` export from `UseCollectionState.ts`.
- Remove redundant `computed` from `UseCollectionState.ts`.
- Remove `timestamp` from `TreeViewFilterEvent` that becomes unnecessary
  after using `shallowRef`.
- Add missing unit tests for `UseTreeViewFilterEvent`.
- Add missing stub for `FilterChangeDetails`.
2023-10-31 13:57:57 +01:00
undergroundwires
77123d8c92 win: change system app removal to hard delete #260
This commit changes the system app removal functionality in privacy.sexy
to perform a hard delete, while preserving the soft-delete logic for
handling residual files.

It improves in-code documentation to facilitate a clearer understanding
of the code execution flow, as the logic for removing system apps has
grown in complexity and length.

Transitioning to a hard-delete approach resolves issues related to
residual links to soft-deleted apps:

- Resolves issue with Edge remaining in the installed apps list (#236).
- Resolves issue with Edge remaining in the programs list (#194).
- Resolves issue with Edge shortcuts persisting in the start menu (#73).

Other changes:

- `RunPowerShell`:
  - Introduce `codeComment` and `revertCodeComment` parameters for
    improved in-code documentation.
- `CommentCode`:
  - Simplify naming to `Comment`.
  - Rename `comment` to `codeComment` for clarity.
  - Add functionality to comment on revert with the `revertCodeComment`
    parameter.
2023-10-30 12:39:10 +01:00
undergroundwires
e72c1c13ea win: improve file delete
This commit unifies the way the files are being deleted by introducing
the `DeleteFiles` function. It refactors existing scripts that are
deleting files to use the new function, to improve their documentation
and increase their safety.

Script changes:

- 'Clear Software Reporter Tool logs':
  - Rename to: 'Clear Google's "Software Reporter Tool" logs'
- 'Clear credentials in Windows Credential Manager':
  - Migrate code to PowerShell, removing the need to delete files.
  - Improve error messages and robustness of the implementation.
- 'Clear Nvidia residual telemetry files':
  - Extract to two scripts for more granularity and better
    documentation:
      1. 'Disable Nvidia telemetry components'
      2. 'Disable Nvidia telemetry drivers'
  - Change the logic so instead of clearing directory contents and
    deleting drivers, it conducts a soft delete for reversibility to
    prioritize user safety.
- 'Remove OneDrive residual files':
  - Improve documentation
- 'Clear primary Windows telemetry file':
  - Rename to 'Clear diagnostics tracking logs'.
  - Add missing file paths seen on modern versions of Windows.
  - Add more documentation.
- 'Clear Windows Update History (`WUAgent`) system logs':
  - Rename to 'Clear Windows update files'.
  - Add more documentation.
- 'Clear Cryptographic Services diagnostic traces':
  - Rename to 'Clear "Cryptographic Services" diagnostic traces'.
  - Add more documentation.

Other changes:

- Improve `DeleteGlob`:
  - Add iteration callbacks for its reusability.
  - Improve its documentation.
  - Make recursion optional.
  - Improve sanity check (verification) logic for given glob when
    granting permissions.
  - Fix granting permissions using wrong variable to find out parent
    directory.
- Improve `IterateGlob`:
  - Use `Get-Item` to get results. This fixes `DeleteDirectory` not
    being able to delete directory itself but just its contents.
  - Introduce and use `recurse` parameter to provide optional recursive
    search logic (to search in subdirectories) using `Get-ChildItem`.
  - Fix wrong PowerShell syntax for `$revert` variable value for
    `revertCode`: replace `true` with `$true`.
  - Order iterated paths based on their length to process the deepest
    item first.
  - Improve handling of missing files with correct/informative outputs
    when granting permissions.
- Improve `SoftDeleteFiles`:
  - Introduce and use `recurse` parameter for explicitness.
  - Fix undefined `$backupFilePath` by replacing it with correct
    `$originalFilePath`.
  - Improve documentation.
- Ensure consistent use of instructive language in code comments.
2023-10-29 18:42:41 +01:00
undergroundwires
e775d68a9b linux: fix string formatting of Firefox configs
This commit fixes some configurations being set wrong values to wrong
YAML notation used for string values.
2023-10-28 13:58:41 +02:00
undergroundwires
f8e5f1a5a2 Fix incorrect tooltip position after window resize
This commit fixes an issue where the tooltip position becomes inaccurate
after resizing the window.

The solution uses `autoUpdate` functionality of `floating-ui` to update
the position automatically on resize events. This function depends on
browser APIs: `IntersectionObserver` and `ResizeObserver`. The official
documentation recommends polyfilling those to support old browsers.

Polyfilling `ResizeObserver` is already part of the codebase, used by
`SizeObserver.vue`. This commit refactors polyfill logic to be reusable
across different components, and reuses it on `TooltipWrapper.vue`.

Polyfilling `IntersectionObserver` is ignored due to this API being
older and more widely supported.
2023-10-27 20:58:07 +02:00
undergroundwires
f4a74f058d win: improve soft file/app delete security #260
This commit improves soft file delete logic:

- Unify logic for soft deleting single files and system apps.
- Rename `RenameSystemFile` templating function to `SoftDeleteFiles` so
  new name gives clarity to:
   - It's not necessarily single file being renamed but can be multiple
     files.
   - It's not necessarily system files being renamed, but can also work
     without granting extra permissions.
- Grant permissions for only files that will be backed up, skipping
  unnecessarily granting permissions to folders/other files. Both
  `SeRestorePrivilege` and `SeTakeownershipPrivileges` are claimed and
  revoked as necessary.
- Make granting permissions optional through `grantPermissions`
  parameter. Do not take permissions if not needed.
- Restore permissions to system default after file is renamed. Before
  both deletion of system apps and renaming system files did not restore
  their original permissions. This might leave user computers
  vulnerable, which is fixed in this commit. It ensures that the
  system's original security posture is preserved.
- Deleting system apps is now independent of `Get-AppxPackage`,
  improving its robustness and enabling their execution once system apps
  are hard-deleted (#260)
- Introduce common way to share glob iteration logic of how the
  directories are being cleaned up. It reuses most of the logic from
  former `DeleteGlob` with some improvements:
  - Simplify call to `Get-ChildItem` by avoiding `-Filter` parameter.
  - Improve reliability of getting parent directory in `DeleteGlob`
    sanity check to use .NET's `[System.IO.Path]` methods.
2023-10-26 18:35:39 +02:00
undergroundwires
80821fca07 Fix compiler failing with nested with expression
The previous implementation of `WithParser` used regex, which struggles
with parsing nested structures correctly. This commit improves
`WithParser` to track and parse all nested `with` expressions.

Other improvements:

- Throw meaningful errors when syntax is wrong. Replacing the prior
  behavior of silently ignoring such issues.
- Remove `I` prefix from related interfaces to align with newer code
  conventions.
- Add more unit tests for `with` expression.
- Improve documentation for templating.
- `ExpressionRegexBuilder`:
  - Use words `capture` and `match` correctly.
  - Fix minor issues revealed by new and improved tests:
     - Change regex for matching anything except surrounding
       whitespaces. The new regex ensures that it works even without
       having any preceeding text.
     - Change regex for capturing pipelines. The old regex was only
       matching (non-greedy) first character of the pipeline in tests,
       new regex matches the full pipeline.
- `ExpressionRegexBuilder.spec.ts`:
  - Ensure consistent way to define `describe` and `it` blocks.
  - Replace `expectRegex` tests, regex expectations test internal
    behavior of the class, not the external.
  - Simplified tests by eliminating the need for UUID suffixes/prefixes.
2023-10-25 19:39:12 +02:00
undergroundwires
dfd4451561 win: improve script environment robustness #221
This commit ensures the script functions as expected, even when invoked
from unexpected environments.

Using `setlocal` initializes a distinct environment for privacy.sexy.
It's strategically placed after the admin privilege check to avoid
unnecessary setup in case of a relaunch. The script concludes with
`endlocal` right before the exit, maintaining a clean environment
throughout its execution and ensuring no unintentional global
environment modifications.

Changes:

- Enhance script's environment robustness.
- Add descriptive comments for script start/end sequences.
2023-10-24 16:56:54 +02:00
undergroundwires
8570b02dde win: prevent updates from reinstalling apps #260
This commit addresses the issue of unwanted applications being
reinstalled during a Windows update. By adding a specific registry
entry, this commit ensures that Windows apps, once removed, do not
return with subsequent updates.

This change ensures more control over the applications present on a
Windows system, particularly after an update, enhancing user experience
and systeam cleanliness.
2023-10-23 16:52:52 +02:00
undergroundwires
d6da406c61 Centralize Electron entry file path configuration
This commit refactors configuration to use centrally defined Electron
entry file path to improve maintainability and reduce duplication.

- Replace the hardcoded file path in the `main` field of `package.json`
  with a reference to the `ELECTRON_ENTRY` environment variable, managed
  by `electron-vite`.
- Update `electron-vite` to version 1.0.28, enabling the use of
  `ELECTRON_ENTRY` environment variable feature (details in
  alex8088/electron-vite#270).
2023-10-22 15:03:58 +02:00
undergroundwires
060e789662 win: improve directory cleanup security
This commit improves the security, reliability, and robustness of
directory cleanup operations on Windows.

The focus is shifted from deleting entire directories to purging their
contents, addressing potential unintended side effects. Previously,
numerous directories were removed, which could destabilize system
behavior.

This improvement has crucial security implications. The prior approach
involved changing ownership and assigning permissions to the directory
itself, leading to an altered and potentially less secure OS security
posture.

Directory removal improvements include:

- Output user-friendly messages.
- Improved ownership and permission handling for file deletion.
- Explicit shared functions for enhanced reliability/security.
- Centralized way to delete glob (wildcard) patterns in Windows.
Notable script improvements:

- 'Clear Steam dumps, logs, and traces':
  - Convert the script to a category to provide more granularity.
  - Improve cache cleaning, ensuring the entire cache directory is
    cleared, not just the log files.
- 'Clear "Temporary Internet Files" (browser cache)':
  - Add more documentation.
  - Grant necessary permissions to folders, fixing errors due to
    lack of permissions before.
- 'Clear Windows Update Medic Service logs':
  - Remove redundant permission grants, as they are unnecessary in
    recent Windows versions.
- 'Clear Server-initiated Healing Events system logs',
  'Clear Windows Update events logs':
  - Merge due to identical functionalities.
  - Add more documentation.
- 'Clear Defender scan (protection) history':
  - Remove the execution with `TrustedInstallerPrivileges`, uniformly
    using `grantPermissions` as with other scripts. This addresses the
    false-positive alerts from Microsoft Defender, as discussed in #264.
- 'Clear "Temporary Internet Files" (browser cache)':
  - Retain `INetCache` and `Temporary Internet Files` directories,
    purging only their contents. This approach aims to resolve the issue
    mentioned in #145, where the absence of these folders could prevent
    Microsoft Office applications from launching.
2023-10-21 17:41:37 +02:00
undergroundwires
e40b9a3cf5 win: fix Microsoft Advertising app removal #200
This commit fixes the issue where the Microsoft Advertising app fails to
be removed using the standard script. The problem is due to Microsoft
Advertising SDK (`Microsoft.Advertising.Xaml`) acting as a framework
package. Such packages are automatically installed when a specific
application requires them, and they cannot be individually removed if
there are applications that depend on them. The only way to effectively
remove this library is by uninstalling the dependent applications.

Key findings:

- On Windows 11 22H2, the issue does not arise as the package does not
  exist.
- On Windows 10 22H2, the user is prompted to delete dependent
  applications, like MSN Weather and Mail And Calendar apps. Once these
  apps are removed, the Microsoft Advertising app is automatically
  removed.

Given the nuances and potential for confusion, this script is removed.
This means that the app will no longer be removed directly but will be
handled as part of the removal of its dependencies.
2023-10-20 16:04:25 +02:00
undergroundwires
237d9944f9 Fix YAML error for site release in CI/CD
Fix the syntax error in the GitHub action script that was caused by
improper multi-line YAML notation. This correction ensures the action
can successfully parse and execute.
2023-10-19 12:33:26 +02:00
undergroundwires
79b46bf210 Improve performance of rendering during search
Optimize the tree view rendering during searches by enhancing the render
queue ordering. This update changes the rendering order to prioritize
visible nodes, leading to faster appearance of these nodes during
searches. The ordering logic now ignores the depth in the hierarchy and
instead focused on the node order. The collapsed check for the node
itself is removed, ensuring that visible collapsed parents are first
while their invisible children are rendered later.
2023-10-18 16:44:49 +02:00
undergroundwires
98a26f9ae4 win: improve system app uninstall /w fallback #260
This commit improves soft deletion of system apps. Before if the package
was missing, it failed to recover or delete system apps. Now, it works
even though if `Get-AppxPackage` returns null (i.e. package is
non-existing), so it can be executed even after a hard delete. This
allows safely introducing hard-delete of system apps (as discussed in
 #260) with still keeping a robust soft-delete as complement.

Before, the script was dependent on `Get-AppxPackage.InstallLocation`,
however a system app can only be located in one of these folders:

- C:\Windows\SystemApps\{PackageFamilyName}
- C:\Windows\{ShortAppName}

To ensure resilience, this commit adjust the script to rename the files
within these directories if Get-AppxPackage fails, this provides a
fallback.
2023-10-17 13:56:32 +02:00
undergroundwires
dbe3c5cfb9 win: improve system app uninstall cleanup #73
- Add documentation about folders.
- Add more user-friendly logging.
- Continue uninstallation if single folder fails (remove throw).
- Continue uninstallation if renaming single file fails.
- Add handling of `Metadata` folder as suggested in #73.
2023-10-16 18:42:29 +02:00
undergroundwires
25d7f7b2a4 Bump dependencies to latest
This commit updates various dependencies to their latest versions.

Other changes include:

- Moved the following from `devDependencies` to `dependencies`:
  - `electron-log`
  - `electron-updater`
- Remove `npm` dependency.
- Code changes:
  - Add type casting in several places to align with the latest
    `typescript` version.
  - Adopt to new return type of `setTimeout`.
- Dependencies not upgraded due to
  `@vue/eslint-config-airbnb-with-typescript` not supporting
  `@eslint-typescript` V6 (see vuejs/eslint-config-airbnb#58):
  - `vue/eslint-config-typescript`
  - `@typescript-eslint/eslint-plugin`
  - `@typescript-eslint/parser`
- Enable video recording for cypress as it's disabled by default since
  13.X.X.
2023-10-16 02:06:19 +02:00
undergroundwires-bot
b76e99ac0f ⬆️ bump everywhere to 0.12.5 2023-10-14 14:17:25 +00:00
undergroundwires
67c3677621 win, linux, mac: fix typos and improve naming
- Use instruction format such as "do this, do that" to provide clear,
  direct instructions. This format minimize confusion and is easy to
  follow. They are specific and leave no room for interpretation,
  stating precisely what needs to be done without ambiguity.
- Fix typos and grammar issues.
- Improve consistency in script and category names.
- Revise sentences for more natural English language flow.
- Change brand name casing to match official branding.
- Change title case (all words start capitalized) to sentence case.
- Prioritize consistency over variations.
- Add minor documentation to explain scripts where the names are not
  clear.
- Add naming guidelines.
2023-10-13 20:14:33 +02:00
undergroundwires
bab6316e76 win: fix and improve AppCompat disabling #255
- Introduce a new parent category: 'Disable Application Compatibility
  framework" for better categorization.
- Move following existing scripts under the new category:
  - Disable Application Impact Telemetry (AIT)
  - Disable steps recorder
  - Disable Inventory Collector
  - Program Compatibility Assistant Service
- Add new scripts new scripts within the same category:
  - Disable Application Compatibility Engine
  - Disable "Program Compatibility Assistant (PCA)" feature
  - Disable "Program Compatibility Assistant Service" (`PcaSvc`)
- Add missing revert codes for:
  - 'Disable steps recorder'
- Fix revert codes for scripts:
  - 'Disable Inventory Collector'
  - 'Disable Application Impact Telemetry (AIT)' (as pointed in #255).
- Add extensive documentation for all related scripts.
- Rename scripts for clarity:
  - 'Disable Inventory Collector' > 'Disable "Inventory Collector"
    task'.
  - 'Program Compatibility Assistant Service' > 'Disable "Program
    Compatibility Assistant Service" (`PcaSvc`) service'.
  - 'Disable steps recorder' > 'Disable Steps Recorder (collects
    screenshots, mouse/keyboard input and UI data)'.
2023-10-12 14:49:35 +02:00
undergroundwires
48730bca05 Implement new UI component for icons #230
- Introduce `AppIcon.vue`, offering improved performance over the
  previous `fort-awesome` dependency. This implementation reduces bundle
  size by 67.31KB (tested for web using `npm run build -- --mode prod`).
- Migrate Font Awesome 5 icons to Font Awesome 6.

This commit facilitates migration to Vue 3.0 (#230) and ensures no Vue
component remains tightly bound to a specific Vue version, enhancing
code portability.

Font Awesome license is not included because Font Awesome revokes its
right:

> "Attribution is no longer required as of Font Awesome 3.0"
>
> Sources:
>
> - https://fontawesome.com/v4/license/ (archived: https://web.archive.org/web/20231003213441/https://fontawesome.com/v4/license/, https://archive.ph/Yy9j5)
> - https://github.com/FortAwesome/Font-Awesome/wiki (archived: https://web.archive.org/web/20231003214646/https://github.com/FortAwesome/Font-Awesome/wiki, https://archive.ph/C6sXv)

This commit removes following third-party production dependencies:

- `@fortawesome/vue-fontawesome`
- `@fortawesome/free-solid-svg-icons`
- `@fortawesome/free-regular-svg-icons`
- `@fortawesome/free-brands-svg-icons`
- `@fortawesome/fontawesome-svg-core`
2023-10-11 18:38:19 +02:00
undergroundwires
698b570ee6 Fix working directory in CI/CD web release
This commit fixes the CI/CD website release process which was failing
due to an incorrect working directory setting. The `working-directory`
is now correctly set within the action workflow, ensuring the `npm run
install-deps` command runs in project root directory where
`package.json` exists.
2023-10-10 15:37:59 +02:00
undergroundwires
a3f11dff18 win: improve app reversion and docs #260
This commit prepares for #260, aiming for a hard delete of system apps,
and necessitating a more reliable app reversion method.

- Improve documentation:
  - Add existence status for latest OS versions.
  - Add command for quick future testing.
  - Use archive links.
  - Document categories.
  - Add documentation to list of default apps to give context about why
    the package is here.
  - Fix wrong store URL for Cortana app.
  - Unify documentation of excluded apps.
- Fix categorization:
  - Categorize uninstallation of Windows store apps.
  - Remove "Zune" category (flatten children apps) to be able to align
    with latest branding.
  - Categorize uninstallation of Candy Crush apps.
  - Categorize uninstallation of OOBE apps.
- Rename:
  - "Uninstall Windows store apps" to "Uninstall Windows apps" as these
    apps are not necessarily store apps.
  - "Xbox Game Bar Plugin appcache" to "Xbox Game Bar Plugin".
  - "Groove Music" to "Windows Media Player".
  - "Movies and TV" to "Movies & TV".
  - "Your Phone" to "Phone Link".
  - "Cred Dialog Host" to "Credentials Dialog Host".
  - "Windows Voice Recorder" to "Windows Sound Recorder".
  - "Remote Desktop" to "Microsoft Remote Desktop"
  - "Microsoft To Do" to "Microsoft To Do: Lists, Tasks & Reminders".
  - "People Hub app (People Experience Host)" to "People Hub app".
  - "My Office" to "Microsoft 365 (Office)".
  - "iHeartRadio" to "iHeart: Radio, Music, Podcasts".
  - "Duolingo" to "Duolingo - Language Lessons".
  - "Photoshop Express" to "Adobe Photoshop Express".
  - "Spotify" to "Spotify - Music and Podcasts".
  - "Windows Alarms and Clock" to "Windows Clock".
  - "OOBE Network Captive Port" to "OOBE Network Captive Portal".
  - "Secure Assessment Browser app (breaks Microsoft Intune/Graph)" to
    "Take a Test app".
  - "Windows 10 Family Safety / Parental Controls" > "Microsoft Family
    Safety / Parental control".
  - "People / People Bar App on taskbar (People Experience Host)" > "My
    People"
  - "MSN News" > "Microsoft News"
  - "Minecraft for Windows 10" > "Minecraft for Windows"
  - "Snip & Sketch" > "Snipping Tool"
  - "Bio enrollment" > "Hello setup UI"
- Fix package names for:
  - `AdobeSystemIncorporated.AdobePhotoshop` >
    `AdobeSystemsIncorporated.AdobePhotoshopExpress`
2023-10-09 16:21:26 +02:00
undergroundwires
5e359c2fb8 win: fix and improve network data usage reset #265
Fix `Clear (Reset) Network Data Usage` trying to delete other files from
Windows system directory.

Changes:

- Precisely target the deletion of `C:\System32\sru\SRUDB.dat`.
- Improve documentation.
- Handle explicitly and better if `DPS` service is missing.
- Rename script from `Clear (Reset) Network Data Usage` to `Clear System
  Resource Usage Monitor (SRUM) data` for clearer representation.
- Migrate script from batchfile to PowerShell for better
  maintainability and readability.
- Add user-friendly output messages.
- Improve script logic to avoid unnecessary service start/stop when the
  file doesn't exist.
2023-10-08 15:55:06 +02:00
undergroundwires
2147eae687 Add developer toolkit UI component
The commit adds a new a UI component that's enabled in development mode.
This component, initially, provides a button that wen clicked, logs all
the script and category names to the console. It helps revising names
used throughout the application.

By having this component in a conditionally rendered component, it's
excluded from the production builds.
2023-10-07 15:14:53 +02:00
undergroundwires
286295128d win: relocate and document SecHealthUI #190
- Move removal of `SecHealthUI` app to "Privacy over security" category.
- Emphasize disruptive behavior in the script name.
- Add comprehensive documentation
2023-10-06 14:02:11 +02:00
undergroundwires
8501495c17 win: improve Edge & OneDrive shortcut removal #73
- Add script to remove Edge shortcuts upon uninstallation.
- Unify OneDrive shortcut removal logic with Edge's, introducing revert
  feature to the OneDrive removal script.
- Add more extensive documentation.
- Rename "Delete OneDrive shortcuts" to "Remove OneDrive shortcuts" to
  have consistent naming.
2023-10-05 11:50:21 +02:00
undergroundwires
888c9166fc win: add removal of Edge assocations #64
This commit introduces scripts for cleaning up file and URL associations
related to Microsoft Edge, enhancing the uninstallation process. The
changes adress the issues detailed in #64, improving system reliability,
integrity and security by preventing lingering associations.

Changes include:

- Introduce scripts to clear Edge browser file and URL associations.
- Provide extensive documentation for related scripts.
- Ensure thorough cleanup of URL, file, OpenWith menu, and toast
  associations.
- Recommend removing Microsoft Edge (Legacy) Dev Tools Client app on
  Strict to align with other Edge legacy removal recommendations.
2023-10-04 11:22:47 +02:00
undergroundwires
e5f6edf405 linux: fix obsolete Firefox DPI script #239
- Replace obsolete "Firefox First party isolation" with "Firefox state
  partitioning".
- Add comprehensive documentation for the new scripts.
- Introduce enabling dynamic First-Party Isolation (dFPI)
- Disable deprecated First-Party Isolation (FPI) to avoid conflicts with
  dFPI.
- Add script to enable Firefox network partitioning to cover
  functionality of older FPI script.
2023-10-03 12:36:06 +02:00
undergroundwires
e8a52f717d win, linux: improve VSCode setting robustness #196
This commit enhances the robustness of setting VSCode configurations,
ensuring consistent and reliable operation even in edge cases, such as
when the settings file is empty. This commit also uniforms behavior of
Linux and Windows modification of VSCode settings.

On Windows:

- Move parameters to on top of scripts to be able to easily test the
  scripts using PowerShell without compiling.
- Add a check to exit the script with an error message if the attempt to
  parse the JSON content fails.
- Omit the `OutString` cmdlet from the pipeline in the script for
  converting JSON file content to a PowerShell object. `Out-String` is
  unnecessary in this context because `Get-Content` already outputs the
  file content as a string array, which `ConvertFrom-Json` effectively.
  Additionally, using `Out-String` could potentially introduce issues by
  concatenating file content into a single string, causing
  `ConvertFrom-Json` to fail when processing pretty-printed JSON. By
  removing `Out-String`, the script is streamlined and potential errors
  are avoided.
- Add logic to handle empty settings file. Add an additional check for
  empty settings file, if the file is empty, the script writes a default
  empty JSON object (`{}`) to the file. The operation is logged to
  ensure transparency, notifying the user of the action taken. This
  change removes fails due to empty setting files.
- When reverting, do not fail if the setting file is missing because it
  means that default settings are already in-place.
- When reverting, show informative message if the key does not exist or
  does not have the value set by privacy.sexy and do not take any
  further action.
- If the desired value is already set, show a message for it and skip
  updating the setting file.

On Linux:

- Handles empty `settings.json` similarly to Windows.
- Add more user friendly error if JSON file cannot be parsed.
2023-10-02 14:33:55 +02:00
undergroundwires
d45750428c win: fix and improve temp dir cleanup #176, #89
This commit improve cleanup of temporary directories on Windows,
addressing issues #176 and #89.

Changes include:

- Fix side-effects caused by this script by clearing the contents of
  directories rather than deleting the directories themselves.
- Add the removal of Prefetch directory contents, which stores temporary
  files and can enhance privacy and free up disk space when cleared.
- Remove the command `del /f /q %localappdata%\Temp\*` due to its
  redundancy.
- Improve the granularity and documentation of cleanup scripts, and
  moving the `Clear temporary Windows files` category up in the hierarchy
  for better structure and clarity.

Co-authored-by: iam-py-test <84232764+iam-py-test@users.noreply.github.com>
2023-10-01 17:42:25 +02:00
undergroundwires
cf55ca9e28 Add Scoop download instructions #174
- Add "Further Installation Options" section.
- Move releases page reference to the new section to keep Get Started
  simple.

Co-authored-by: MrEddX <101912712+Zliced13@users.noreply.github.com>
2023-09-29 14:03:07 +02:00
undergroundwires
3e5239f7d3 Add SAST security checks with SECURITY.md #178
This commit incorporates Static Analysis Security Testing (SAST) using
CodeQL. This integration will enforce consistent security assessments
with every change and on a predetermined schedule.

This commit also involves a restructure of security checks. The existing
security-checks workflow is renamed to better reflect its functionality
related to dependency audits.

These changes will enhance the project's resilience against potential
vulnerabilities in both the codebase and third-party dependencies.

Changes include:

- Remove older LGTM badge that's replaced by SAST checks.
- Rename `checks.security.yaml` to `checks.security.dependencies.yaml`,
  reinforcing the focus on dependency audits.
- Update `README.md`, ensuring the clear representation of security
  check statuses, including new SAST integration.
- Add new `SECURITY.md`, establishing the protocol for reporting
  vulnerabilities and outlining the project's commitment to robust
  security testing.
- Enhance `docs/tests.md` with detailed information on the newly
  integrated security checks.
- Add reference to SECURITY.md in README.md.
2023-09-28 15:19:09 +02:00
undergroundwires
7669985f8e Fix Docker build and improve checks #220
This commit improves multiple aspects of Docker builds:

- Enable artifact output validation for Dockerfile.
- Correct the path references in Dockerfile for the distribution
  directory.
- Add Dockerfile specific indentation rules to `.editorconfig`.
- Use `npm run install-deps` for dependency installation, enhancing
  build reliability.
- Add automation script `verify-web-server-status.js` to verify running
  web server on given URL.
- Introduce automated build verification for Dockerfile:
  - On macOS, install Docker with colima as the container runtime
    because default agents do not include Docker and Docker runtime is
    not installed due to licensing issues (see actions/runner-images#17).
  - On Windows, there's no Linux container support (actions/runner#904,
    actions/runner-images#1143), so keep the checks for macOS and Ubuntu
    only.
2023-09-27 19:53:40 +02:00
undergroundwires-bot
5047c9b6e7 ⬆️ bump everywhere to 0.12.4 2023-09-26 11:45:04 +00:00
527 changed files with 24531 additions and 15876 deletions

View File

@@ -5,3 +5,7 @@ end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
[{Dockerfile}]
indent_style = space
indent_size = 4

View File

@@ -10,7 +10,7 @@ module.exports = {
},
extends: [
// Vue specific rules, eslint-plugin-vue
'plugin:vue/essential',
'plugin:vue/vue3-recommended',
// Extends eslint-config-airbnb
'@vue/eslint-config-airbnb-with-typescript',

View File

@@ -8,4 +8,5 @@ runs:
-
name: Run `npm ci` with retries
shell: bash
run: npm run install-deps -- --ci --root-directory "${{ inputs.working-directory }}"
run: npm run install-deps -- --ci
working-directory: ${{ inputs.working-directory }}

View File

@@ -3,6 +3,6 @@ runs:
steps:
-
name: Setup node
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 16.x

View File

@@ -1,4 +1,4 @@
name: build-checks
name: checks.build
on:
push:
@@ -21,7 +21,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
@@ -49,7 +49,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
@@ -68,3 +68,33 @@ jobs:
-
name: Verify bundled desktop build artifacts
run: npm run check:verify-build-artifacts -- --electron-bundled
build-docker:
strategy:
matrix:
os: [ macos, ubuntu ] # Windows runners do not support Linux containers
fail-fast: false # Allows to see results from other combinations
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Install Docker on macOS
if: 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: Check server is up and returns HTTP 200
run: node ./scripts/verify-web-server-status.js --url http://localhost:8080

View File

@@ -15,7 +15,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -10,7 +10,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -18,7 +18,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -14,7 +14,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
@@ -42,7 +42,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -1,4 +1,4 @@
name: security-checks
name: checks.security.dependencies
on:
push:
@@ -13,7 +13,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -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 }}"

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ${{ matrix.os }}-latest
steps:
-
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
ref: master # otherwise it defaults to the version tag missing bump commit
fetch-depth: 0 # fetch all history

View File

@@ -10,7 +10,7 @@ jobs:
steps:
-
name: "Infrastructure: Checkout"
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
path: aws
repository: undergroundwires/aws-static-site-with-cd
@@ -75,7 +75,7 @@ jobs:
working-directory: aws
-
name: "App: Checkout"
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
path: app
ref: master # otherwise we don't get version bump commit
@@ -102,7 +102,7 @@ jobs:
-
name: "App: Deploy to S3"
shell: bash
run: >-
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.'

View File

@@ -14,7 +14,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
@@ -24,3 +24,41 @@ jobs:
-
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: actions/upload-artifact@v3
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: actions/upload-artifact@v3
with:
name: e2e-videos-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}

View File

@@ -16,7 +16,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -14,7 +14,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Set-up node
uses: ./.github/actions/setup-node

14
.gitignore vendored
View File

@@ -1,5 +1,13 @@
node_modules
# Application build artifacts
/dist-*/
.vs
# npm
node_modules
# Visual Studio Code
.vscode/**/*
!.vscode/extensions.json
!.vscode/extensions.json
# draw.io
*.bkp
*.dtmp

View File

@@ -1,5 +1,78 @@
# Changelog
## 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)

View File

@@ -43,6 +43,7 @@ You have two alternatives:
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
- 💡 You should use existing shared functions for most of the operations, like `DisableService` for disabling services, to maintain code consistency and efficiency.
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).

View File

@@ -1,13 +1,16 @@
# Build
FROM node:lts-alpine as build-stage
FROM node:lts-alpine AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
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 /app/dist /usr/share/nginx/html
FROM nginx:stable-alpine AS production-stage
COPY --from=build-stage /dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -16,14 +16,6 @@
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
/>
</a>
<!-- Code quality -->
<br />
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript" target="_blank" rel="noopener noreferrer">
<img
alt="Language grade: JavaScript/TypeScript"
src="https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18"
/>
</a>
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability" target="_blank" rel="noopener noreferrer">
<img
alt="Maintainability"
@@ -50,6 +42,20 @@
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
/>
</a>
<!-- Security checks -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.sast.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Status of dependency security checks"
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.security.sast/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.dependencies.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Status of Static Analysis Security Testing (SAST)"
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.security.dependencies/badge.svg"
/>
</a>
<!-- Checks -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
@@ -58,16 +64,10 @@
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Security checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
<img
alt="Build checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
alt="Status of build checks"
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.build/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.desktop-runtime-errors.yaml" target="_blank" rel="noopener noreferrer">
@@ -122,7 +122,7 @@
## Get started
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.3/privacy.sexy-Setup-0.12.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.3/privacy.sexy-0.12.3.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.3/privacy.sexy-0.12.3.AppImage).
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-Setup-0.12.7.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.AppImage). For more options, see [here](#additional-install-options).
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
@@ -150,6 +150,25 @@ Online version does not require to run any software on your computer. Offline ve
**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.
@@ -157,3 +176,7 @@ Refer to [development.md](./docs/development.md) for Docker usage and reading mo
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).

31
SECURITY.md Normal file
View File

@@ -0,0 +1,31 @@
# Security Policy
privacy.sexy takes security seriously. Commitment is made to address all security issues with urgency. Responsible reporting of any discovered vulnerabilities in the project is highly encouraged.
## 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 receipt of a security report, the following actions will be taken:
- The report will be confirmed, identifying the affected components.
- The impact and severity of the issue will be assessed.
- Work on a fix and plan a release to address the vulnerability will be initiated.
- The reporter will be kept updated about the progress.
## Testing
Regular and extensive testing is conducted to ensure robust security in the project. Information about testing practices can be found in the [Testing Documentation](./docs/tests.md).
## Support
For additional assistance or any unanswered questions, [submit a GitHub issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose). Security concerns are a priority, and necessary support to address them is assured.
---
Active contribution to the safety and security of privacy.sexy is thanked. This collaborative effort keeps the project resilient and trustworthy for all.

5
cypress-dirs.json Normal file
View File

@@ -0,0 +1,5 @@
{
"base": "tests/e2e",
"videos": "tests/e2e/videos",
"screenshots": "tests/e2e/videos"
}

View File

@@ -1,15 +1,31 @@
import { defineConfig } from 'cypress';
import ViteConfig from './vite.config';
const CYPRESS_BASE_DIR = 'tests/e2e/';
import cypressDirs from './cypress-dirs.json' assert { type: 'json' };
export default defineConfig({
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
fixturesFolder: `${cypressDirs.base}/fixtures`,
screenshotsFolder: cypressDirs.screenshots,
video: true,
videosFolder: cypressDirs.videos,
e2e: {
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
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;
}

View File

@@ -174,3 +174,19 @@
- `endCode:` *`string`* (**required**)
- Code that'll be inserted at the end of user created script.
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
## Naming guidelines
- Prioritize consistency throughout all names.
- Use an instruction format like "do this, do that" for clear, direct guidance. This approach reduces potential confusion and offers easy-to-follow steps. It provides specific, unambiguous instructions.
- Ensure brand names adhere to their official casing.
- Choose clear and uncomplicated language.
- Favor the terms:
- "Disable" over "Turn off"
- "Configure" over "Set up"
- "Clear" over "Erase" or "Clean"
- "Minimize" over "Limit" or "Reduce" (when it enhances clarity)
- "Remove" over "Uninstall"
- Structure your phrases for clarity.
- For instance, "Disable XX telemetry" or "Clear XX data" are preferred over "Clear data from XX", "Disable telemetry in XX", or "Clear data of XX".
- Use sentence case rather than Title Case.

View File

@@ -60,6 +60,7 @@ See [ci-cd.md](./ci-cd.md) for more information.
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
@@ -81,11 +82,12 @@ See [ci-cd.md](./ci-cd.md) for more information.
#### Automation scripts
- [**`node scripts/print-dist-dir.js [-- <options>]`**](../scripts/print-dist-dir.js):
- [**`node scripts/print-dist-dir.js [<options>]`**](../scripts/print-dist-dir.js):
- Determines the absolute path of a distribution directory based on CLI arguments and outputs its absolute path.
- Primarily used by automation scripts.
- [**`npm run check:verify-build-artifacts [-- <options>]`**](../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

View File

@@ -11,6 +11,8 @@ The presentation layer uses an event-driven architecture for bidirectional react
## 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 and helpers.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
@@ -20,8 +22,7 @@ The presentation layer uses an event-driven architecture for bidirectional react
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
- [**`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/main.ts): Main entry for Electron, managing application windows and lifecycle events.
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
@@ -70,10 +71,11 @@ 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 Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
- For singletons, invoke the factory method: `inject(symbolKey)()`.
- For transients, directly inject: `inject(symbolKey)`.
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

View File

@@ -2,79 +2,142 @@
## Benefits of templating
- Generating scripts by sharing code to increase best-practice usage and maintainability.
- Creating self-contained scripts without cross-dependencies.
- Use of pipes for writing cleaner code and letting pipes do dirty work.
- **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
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
- E.g. `Hello {{ $name }} !`
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
- Functions enables usage of expressions.
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
- Expressions inside expressions (nested templates) are supported.
- An expression can output another expression that will also be compiled.
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
**Syntax:**
```go
{{ with $condition }}
echo {{ $text }}
{{ end }}
```
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
A simple function example:
Parameter substitution dynamically replaces variable references with their corresponding values in the script.
**Example function:**
```yaml
function: EchoArgument
name: DisplayTextFunction
parameters:
- name: 'argument'
code: Hello {{ $argument }} !
- name: 'text'
code: echo {{ $text }}
```
It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
```yaml
script: Echo script
call:
function: EchoArgument
parameters:
argument: World
```
A function can call other functions such as:
```yaml
-
function: CallerFunction
parameters:
- name: 'value'
call:
function: EchoArgument
parameters:
argument: {{ $value }}
-
function: EchoArgument
parameters:
- name: 'argument'
code: Hello {{ $argument }} !
```
Invoking `DisplayTextFunction` with `text` set to `"Hello, world!"` would result in `echo "Hello, World!"`.
### with
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
The `with` expression enables conditional rendering and provides a context variable for simpler code.
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
**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 }}
```
It supports multiline text inside the block. You can have something like:
**Multiline text:**
It supports multiline text inside the block. You can write something like:
```go
{{ with $argument }}
@@ -83,7 +146,9 @@ It supports multiline text inside the block. You can have something like:
{{ end }}
```
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
**Inner expressions:**
You can also embed other expressions inside its block, such as [parameter substitution](#parameter-substitution):
```go
{{ with $condition }}
@@ -91,32 +156,44 @@ You can also use other expressions inside its block, such as [parameter substitu
{{ end }}
```
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
This also includes nesting `with` statements:
Example:
```yaml
function: FunctionThatOutputsConditionally
parameters:
- name: 'argument'
optional: true
code: |-
{{ with $argument }}
Value is: {{ . }}
```go
{{ with $condition1 }}
Value of $condition1: {{ . }}
{{ with $condition2 }}
Value of $condition2: {{ . }}
{{ end }}
{{ end }}
```
### Pipes
- Pipes are functions available for handling text.
- Allows stacking actions one after another also known as "chaining".
- Like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe.
- You cannot create pipes. [A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
- ❗ Pipe names must be camelCase without any space or special characters.
- **Existing pipes**
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
- `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`).
- **Example usages**
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
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 (`"`).

View File

@@ -56,6 +56,11 @@ These checks validate various qualities like runtime execution, building process
- 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.
@@ -63,21 +68,23 @@ These checks validate various qualities like runtime execution, building process
- [`./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.
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
- [`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.
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
- [`/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.

View File

@@ -8,15 +8,21 @@ 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 DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
const ELECTRON_DIST_SUBDIRECTORIES = {
main: resolveElectronDistSubdirectory('main'),
preload: resolveElectronDistSubdirectory('preload'),
renderer: resolveElectronDistSubdirectory('renderer'),
};
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.cjs');
export default defineConfig({
main: getSharedElectronConfig({
distDirSubfolder: 'main',
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.main,
entryFilePath: MAIN_ENTRY_FILE,
}),
preload: getSharedElectronConfig({
distDirSubfolder: 'preload',
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.preload,
entryFilePath: PRELOAD_ENTRY_FILE,
}),
renderer: mergeConfig(
@@ -25,7 +31,7 @@ export default defineConfig({
}),
{
build: {
outDir: resolve(DIST_DIR, 'renderer'),
outDir: ELECTRON_DIST_SUBDIRECTORIES.renderer,
rollupOptions: {
input: {
index: WEB_INDEX_HTML_PATH,
@@ -42,7 +48,7 @@ function getSharedElectronConfig(options: {
}): UserConfig {
return {
build: {
outDir: resolve(DIST_DIR, options.distDirSubfolder),
outDir: options.distDirSubfolder,
lib: {
entry: options.entryFilePath,
},
@@ -64,6 +70,11 @@ function getSharedElectronConfig(options: {
};
}
function resolvePathFromProjectRoot(pathSegment: string) {
function resolvePathFromProjectRoot(pathSegment: string): string {
return resolve(__dirname, pathSegment);
}
function resolveElectronDistSubdirectory(subDirectory: string): string {
const electronDistDir = resolvePathFromProjectRoot(distDirs.electronUnbundled);
return resolve(electronDistDir, subDirectory);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

10553
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,11 @@
{
"name": "privacy.sexy",
"version": "0.12.3",
"version": "0.12.7",
"private": true,
"slogan": "Now you have the choice",
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
"author": "undergroundwires",
"type": "module",
"main": "./dist-electron-unbundled/main/index.cjs",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
@@ -25,7 +24,7 @@
"electron:preview": "electron-vite preview",
"electron:prebuild": "electron-vite build",
"electron:build": "electron-builder",
"lint:eslint": "eslint . --ignore-path .gitignore",
"lint:eslint": "eslint . --max-warnings=0 --ignore-path .gitignore",
"lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
@@ -35,77 +34,74 @@
},
"dependencies": {
"@floating-ui/vue": "^1.0.2",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^2.0.9",
"@juggle/resize-observer": "^3.4.0",
"ace-builds": "^1.23.4",
"ace-builds": "^1.30.0",
"cross-fetch": "^4.0.0",
"electron-log": "^4.4.8",
"electron-progressbar": "^2.1.0",
"electron-updater": "^6.1.4",
"file-saver": "^2.0.5",
"markdown-it": "^13.0.1",
"npm": "^9.8.1",
"vue": "^2.7.14"
"markdown-it": "^13.0.2",
"vue": "^3.3.7"
},
"devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@rushstack/eslint-patch": "^1.3.2",
"@types/ace": "^0.0.48",
"@rushstack/eslint-patch": "^1.5.1",
"@types/ace": "^0.0.49",
"@types/file-saver": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue2": "^2.2.0",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "^1.3.6",
"autoprefixer": "^10.4.15",
"cypress": "^12.17.2",
"electron": "^25.3.2",
"electron-builder": "^24.6.3",
"@vue/test-utils": "^2.4.1",
"autoprefixer": "^10.4.16",
"cypress": "^13.3.1",
"electron": "^27.0.0",
"electron-builder": "^24.6.4",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-log": "^4.4.8",
"electron-updater": "^6.1.4",
"electron-vite": "^1.0.27",
"eslint": "^8.46.0",
"eslint-plugin-cypress": "^2.14.0",
"eslint-plugin-vue": "^9.6.0",
"eslint-plugin-vuejs-accessibility": "^1.2.0",
"icon-gen": "^3.0.1",
"electron-vite": "^1.0.28",
"eslint": "^8.51.0",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-vue": "^9.17.0",
"eslint-plugin-vuejs-accessibility": "^2.2.0",
"icon-gen": "^4.0.0",
"jsdom": "^22.1.0",
"markdownlint-cli": "^0.35.0",
"postcss": "^8.4.28",
"remark-cli": "^11.0.0",
"markdownlint-cli": "^0.37.0",
"postcss": "^8.4.31",
"remark-cli": "^12.0.0",
"remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^5.1.2",
"remark-validate-links": "^12.1.1",
"sass": "^1.64.1",
"start-server-and-test": "^2.0.0",
"remark-validate-links": "^13.0.0",
"sass": "^1.69.3",
"start-server-and-test": "^2.0.1",
"svgexport": "^0.4.2",
"terser": "^5.19.2",
"tslib": "~2.4.0",
"typescript": "~4.6.2",
"vite": "^4.4.9",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8",
"terser": "^5.21.0",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^4.4.11",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.19",
"yaml-lint": "^1.7.0"
},
"//devDependencies": {
"terser": "Used by @vitejs/plugin-legacy for minification",
"typescript": [
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
],
"tslib": "Cannot upgrade to > 2.4.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252",
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60",
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60"
"@rushstack/eslint-patch": "Needed by @vue/eslint-config-typescript",
"@vue/eslint-config-typescript": "Cannot upgrade to 12.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58"
},
"homepage": "https://privacy.sexy",
"repository": {
"type": "git",
"url": "https://github.com/undergroundwires/privacy.sexy.git"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
},
"//optionalDependencies": {
"dmg-license": "Required by `electron-builder` for DMG builds on macOS, https://github.com/electron-userland/electron-builder/issues/6489, https://github.com/electron-userland/electron-builder/issues/6520"
}
}

View File

@@ -0,0 +1,62 @@
/**
* 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]
*
* Options:
* --url URL of the server to check
*/
import { get } from 'http';
const MAX_RETRIES = 30;
const RETRY_DELAY_IN_SECONDS = 3;
const URL_PARAMETER_NAME = '--url';
function checkServer(currentRetryCount = 1) {
const serverUrl = getServerUrl();
console.log(`Requesting ${serverUrl}...`);
get(serverUrl, (res) => {
if (res.statusCode === 200) {
console.log('🎊 Success: The server is up and returned HTTP 200.');
process.exit(0);
} else {
console.log(`Server returned HTTP status code ${res.statusCode}.`);
retry(currentRetryCount);
}
}).on('error', (err) => {
console.error('Error making the request:', err);
retry(currentRetryCount);
});
}
function retry(currentRetryCount) {
console.log(`Attempt ${currentRetryCount}/${MAX_RETRIES}:`);
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
const remainingTime = (MAX_RETRIES - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
console.log(`Time remaining before timeout: ${remainingTime}s`);
if (currentRetryCount < MAX_RETRIES) {
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
} else {
console.log('Failure: The server at did not return HTTP 200 within the allocated time. Exiting.');
process.exit(1);
}
}
function getServerUrl() {
const urlIndex = process.argv.indexOf(URL_PARAMETER_NAME);
if (urlIndex === -1 || urlIndex === process.argv.length - 1) {
console.error(`Parameter "${URL_PARAMETER_NAME}" is not provided.`);
process.exit(1);
}
return process.argv[urlIndex + 1];
}
checkServer();

View File

@@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory {
private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetterType) {
if (!costlyGetter) {
throw new Error('missing getter');
}
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
}

View File

@@ -1,7 +1,5 @@
// Compares to Array<T> objects for equality, ignoring order
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('missing first array'); }
if (!array2) { throw new Error('missing second array'); }
const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2);
@@ -12,8 +10,6 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
// Compares to Array<T> objects for equality in same order
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('missing first array'); }
if (!array2) { throw new Error('missing second array'); }
if (array1.length !== array2.length) {
return false;
}

View File

@@ -20,23 +20,30 @@ export abstract class CustomError extends Error {
}
}
export const Environment = {
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) {
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
const setPrototypeOf = Environment.getSetPrototypeOf();
if (!functionExists(setPrototypeOf)) {
// 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 = Environment.getCaptureStackTrace();
if (!functionExists(captureStackTrace)) {
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;
@@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) {
captureStackTrace(target, target.constructor);
}
function functionExists(func: unknown): boolean {
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
// eslint-disable-next-line @typescript-eslint/ban-types
function isFunction(func: unknown): func is Function {
return typeof func === 'function';
}

View File

@@ -54,9 +54,6 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>,
) {
if (value === undefined || value === null) {
throw new Error('absent enum value');
}
if (!(value in enumVariable)) {
throw new RangeError(`enum value "${value}" is out of range`);
}

View File

@@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) {
const getter = this.getters.get(language);
if (!getter) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
const getter = this.getters.get(language);
const instance = getter();
return instance;
}
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage);
if (!getter) {
throw new Error('missing getter');
}
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}

View File

@@ -0,0 +1,27 @@
import { PlatformTimer } from './PlatformTimer';
import { TimeoutType, Timer } from './Timer';
export function batchedDebounce<T>(
callback: (batches: readonly T[]) => void,
waitInMs: number,
timer: Timer = PlatformTimer,
): (arg: T) => void {
let lastTimeoutId: TimeoutType | undefined;
let batches: Array<T> = [];
return (arg: T) => {
batches.push(arg);
const later = () => {
callback(batches);
batches = [];
lastTimeoutId = undefined;
};
if (lastTimeoutId !== undefined) {
timer.clearTimeout(lastTimeoutId);
}
lastTimeoutId = timer.setTimeout(later, waitInMs);
};
}

View File

@@ -0,0 +1,7 @@
import { Timer } from './Timer';
export const PlatformTimer: Timer = {
setTimeout: (callback, ms) => setTimeout(callback, ms),
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
dateNow: () => Date.now(),
};

View File

@@ -1,47 +1,29 @@
import { Timer, TimeoutType } from './Timer';
import { PlatformTimer } from './PlatformTimer';
export type CallbackType = (..._: unknown[]) => void;
export function throttle(
callback: CallbackType,
waitInMs: number,
timer: ITimer = NodeTimer,
timer: Timer = PlatformTimer,
): CallbackType {
const throttler = new Throttler(timer, waitInMs, callback);
return (...args: unknown[]) => throttler.invoke(...args);
}
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
export type TimeoutType = ReturnType<typeof setTimeout>;
export interface ITimer {
setTimeout: (callback: () => void, ms: number) => TimeoutType;
clearTimeout: (timeoutId: TimeoutType) => void;
dateNow(): number;
}
const NodeTimer: ITimer = {
setTimeout: (callback, ms) => setTimeout(callback, ms),
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
dateNow: () => Date.now(),
};
interface IThrottler {
invoke: CallbackType;
}
class Throttler implements IThrottler {
private queuedExecutionId: TimeoutType;
class Throttler {
private queuedExecutionId: TimeoutType | undefined;
private previouslyRun: number;
constructor(
private readonly timer: ITimer,
private readonly timer: Timer,
private readonly waitInMs: number,
private readonly callback: CallbackType,
) {
if (!timer) { throw new Error('missing timer'); }
if (!waitInMs) { throw new Error('missing delay'); }
if (waitInMs < 0) { throw new Error('negative delay'); }
if (!callback) { throw new Error('missing callback'); }
}
public invoke(...args: unknown[]): void {

View File

@@ -0,0 +1,8 @@
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
export type TimeoutType = ReturnType<typeof setTimeout>;
export interface Timer {
setTimeout: (callback: () => void, ms: number) => TimeoutType;
clearTimeout: (timeoutId: TimeoutType) => void;
dateNow(): number;
}

View File

@@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication,
initialContext: OperatingSystem,
) {
validateApp(app);
this.states = initializeStates(app);
this.changeContext(initialContext);
}
@@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext {
if (this.currentOs === os) {
return;
}
this.collection = this.app.getCollection(os);
if (!this.collection) {
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
}
const collection = this.app.getCollection(os);
this.collection = collection;
const event: IApplicationContextChangedEvent = {
newState: this.states[os],
oldState: this.states[this.currentOs],
@@ -49,12 +46,6 @@ export class ApplicationContext implements IApplicationContext {
}
}
function validateApp(app: IApplication) {
if (!app) {
throw new Error('missing app');
}
}
function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) {

View File

@@ -10,18 +10,23 @@ export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current,
environment = RuntimeEnvironment.CurrentEnvironment,
): Promise<IApplicationContext> {
if (!factory) { throw new Error('missing factory'); }
if (!environment) { throw new Error('missing environment'); }
const app = await factory.getApp();
const os = getInitialOs(app, environment.os);
return new ApplicationContext(app, os);
}
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
function getInitialOs(
app: IApplication,
currentOs: OperatingSystem | undefined,
): OperatingSystem {
const supportedOsList = app.getSupportedOsList();
if (supportedOsList.includes(currentOs)) {
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);

View File

@@ -4,23 +4,48 @@ import { UserFilter } from './Filter/UserFilter';
import { IUserFilter } from './Filter/IUserFilter';
import { ApplicationCode } from './Code/ApplicationCode';
import { UserSelection } from './Selection/UserSelection';
import { IUserSelection } from './Selection/IUserSelection';
import { ICategoryCollectionState } from './ICategoryCollectionState';
import { IApplicationCode } from './Code/IApplicationCode';
import { UserSelectionFacade } from './Selection/UserSelectionFacade';
export class CategoryCollectionState implements ICategoryCollectionState {
public readonly os: OperatingSystem;
public readonly code: IApplicationCode;
public readonly selection: IUserSelection;
public readonly selection: UserSelection;
public readonly filter: IUserFilter;
public constructor(readonly collection: ICategoryCollection) {
this.selection = new UserSelection(collection, []);
this.code = new ApplicationCode(this.selection, collection.scripting);
this.filter = new UserFilter(collection);
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<typeof ApplicationCode>
) => IApplicationCode;
const DefaultCodeFactory: CodeFactory = (...params) => new ApplicationCode(...params);
export type SelectionFactory = (
...params: ConstructorParameters<typeof UserSelectionFacade>
) => UserSelection;
const DefaultSelectionFactory: SelectionFactory = (
...params
) => new UserSelectionFacade(...params);
export type FilterFactory = (
...params: ConstructorParameters<typeof UserFilter>
) => IUserFilter;
const DefaultFilterFactory: FilterFactory = (...params) => new UserFilter(...params);

View File

@@ -1,7 +1,7 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
import { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
@@ -17,15 +17,12 @@ export class ApplicationCode implements IApplicationCode {
private scriptPositions = new Map<SelectedScript, CodePosition>();
constructor(
userSelection: IReadOnlyUserSelection,
selection: ReadonlyScriptSelection,
private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
) {
if (!userSelection) { throw new Error('missing userSelection'); }
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
if (!generator) { throw new Error('missing generator'); }
this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => {
this.setCode(selection.selectedScripts);
selection.changed.on((scripts) => {
this.setCode(scripts);
});
}

View File

@@ -1,6 +1,6 @@
import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { SelectedScript } from '../../Selection/SelectedScript';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { ICodeChangedEvent } from './ICodeChangedEvent';
export class CodeChangedEvent implements ICodeChangedEvent {
@@ -36,7 +36,18 @@ export class CodeChangedEvent implements ICodeChangedEvent {
}
public getScriptPositionInCode(script: IScript): ICodePosition {
return this.scripts.get(script);
return this.getPositionById(script.id);
}
private getPositionById(scriptId: string): ICodePosition {
const position = [...this.scripts.entries()]
.filter(([s]) => s.id === scriptId)
.map(([, pos]) => pos)
.at(0);
if (!position) {
throw new Error('Unknown script: Position could not be found for the script');
}
return position;
}
}

View File

@@ -3,9 +3,9 @@ import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePo
export interface ICodeChangedEvent {
readonly code: string;
addedScripts: ReadonlyArray<IScript>;
removedScripts: ReadonlyArray<IScript>;
changedScripts: ReadonlyArray<IScript>;
readonly addedScripts: ReadonlyArray<IScript>;
readonly removedScripts: ReadonlyArray<IScript>;
readonly changedScripts: ReadonlyArray<IScript>;
isEmpty(): boolean;
getScriptPositionInCode(script: IScript): ICodePosition;
}

View File

@@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder {
return this;
}
const lines = code.match(/[^\r\n]+/g);
this.lines.push(...lines);
if (lines) {
this.lines.push(...lines);
}
return this;
}

View File

@@ -1,7 +1,7 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
export interface IUserScript {
code: string;
scriptPositions: Map<SelectedScript, ICodePosition>;
readonly code: string;
readonly scriptPositions: Map<SelectedScript, ICodePosition>;
}

View File

@@ -1,9 +1,10 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { IUserScript } from './IUserScript';
export interface IUserScriptGenerator {
buildCode(
selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition): IUserScript;
scriptingDefinition: IScriptingDefinition,
): IUserScript;
}

View File

@@ -1,6 +1,6 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { CodePosition } from '../Position/CodePosition';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { IUserScript } from './IUserScript';
@@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator {
selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition,
): IUserScript {
if (!selectedScripts) { throw new Error('missing scripts'); }
if (!scriptingDefinition) { throw new Error('missing definition'); }
if (!selectedScripts.length) {
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
}
@@ -68,8 +66,19 @@ function appendSelection(
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
const { script } = selection;
const name = selection.revert ? `${script.name} (revert)` : script.name;
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
const scriptCode = getSelectedCode(selection);
return builder
.appendLine()
.appendFunction(name, scriptCode);
}
function getSelectedCode(selection: SelectedScript): string {
const { code } = selection.script;
if (!selection.revert) {
return code.execute;
}
if (!code.revert) {
throw new Error('Reverted script lacks revert code.');
}
return code.revert;
}

View File

@@ -1,37 +1,37 @@
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { FilterActionType } from './FilterActionType';
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails';
import {
IFilterChangeDetails, IFilterChangeDetailsVisitor,
ApplyFilterAction, ClearFilterAction,
} from './IFilterChangeDetails';
export class FilterChange implements IFilterChangeDetails {
public static forApply(filter: IFilterResult) {
if (!filter) {
throw new Error('missing filter');
}
return new FilterChange(FilterActionType.Apply, filter);
public static forApply(
filter: IFilterResult,
): IFilterChangeDetails {
return new FilterChange({ type: FilterActionType.Apply, filter });
}
public static forClear() {
return new FilterChange(FilterActionType.Clear);
public static forClear(): IFilterChangeDetails {
return new FilterChange({ type: FilterActionType.Clear });
}
private constructor(
public readonly actionType: FilterActionType,
public readonly filter?: IFilterResult,
) { }
private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { }
public visit(visitor: IFilterChangeDetailsVisitor): void {
if (!visitor) {
throw new Error('missing visitor');
}
switch (this.actionType) {
switch (this.action.type) {
case FilterActionType.Apply:
visitor.onApply(this.filter);
if (visitor.onApply) {
visitor.onApply(this.action.filter);
}
break;
case FilterActionType.Clear:
visitor.onClear();
if (visitor.onClear) {
visitor.onClear();
}
break;
default:
throw new Error(`Unknown action type: ${this.actionType}`);
throw new Error(`Unknown action: ${this.action}`);
}
}
}

View File

@@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
import { FilterActionType } from './FilterActionType';
export interface IFilterChangeDetails {
readonly actionType: FilterActionType;
readonly filter?: IFilterResult;
readonly action: FilterAction;
visit(visitor: IFilterChangeDetailsVisitor): void;
}
export interface IFilterChangeDetailsVisitor {
onClear(): void;
onApply(filter: IFilterResult): void;
readonly onClear?: () => void;
readonly onApply?: (filter: IFilterResult) => void;
}
export type ApplyFilterAction = {
readonly type: FilterActionType.Apply,
readonly filter: IFilterResult;
};
export type ClearFilterAction = {
readonly type: FilterActionType.Clear,
};
export type FilterAction = ApplyFilterAction | ClearFilterAction;

View File

@@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult {
public readonly query: string,
) {
if (!query) { throw new Error('Query is empty or undefined'); }
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
}
public hasAnyMatches(): boolean {

View File

@@ -1,18 +1,18 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
import { ReadonlyUserSelection, UserSelection } from './Selection/UserSelection';
import { IApplicationCode } from './Code/IApplicationCode';
export interface IReadOnlyCategoryCollectionState {
readonly code: IApplicationCode;
readonly os: OperatingSystem;
readonly filter: IReadOnlyUserFilter;
readonly selection: IReadOnlyUserSelection;
readonly selection: ReadonlyUserSelection;
readonly collection: ICategoryCollection;
}
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
readonly filter: IUserFilter;
readonly selection: IUserSelection;
readonly selection: UserSelection;
}

View File

@@ -0,0 +1,11 @@
import { ICategory } from '@/domain/ICategory';
import { CategorySelectionChangeCommand } from './CategorySelectionChange';
export interface ReadonlyCategorySelection {
areAllScriptsSelected(category: ICategory): boolean;
isAnyScriptSelected(category: ICategory): boolean;
}
export interface CategorySelection extends ReadonlyCategorySelection {
processChanges(action: CategorySelectionChangeCommand): void;
}

View File

@@ -0,0 +1,15 @@
type CategorySelectionStatus = {
readonly isSelected: true;
readonly isReverted: boolean;
} | {
readonly isSelected: false;
};
export interface CategorySelectionChange {
readonly categoryId: number;
readonly newStatus: CategorySelectionStatus;
}
export interface CategorySelectionChangeCommand {
readonly changes: readonly CategorySelectionChange[];
}

View File

@@ -0,0 +1,60 @@
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { ScriptSelection } from '../Script/ScriptSelection';
import { ScriptSelectionChange } from '../Script/ScriptSelectionChange';
import { CategorySelection } from './CategorySelection';
import { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
export class ScriptToCategorySelectionMapper implements CategorySelection {
constructor(
private readonly scriptSelection: ScriptSelection,
private readonly collection: ICategoryCollection,
) {
}
public areAllScriptsSelected(category: ICategory): boolean {
const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) {
return false;
}
const scripts = category.getAllScriptsRecursively();
if (selectedScripts.length < scripts.length) {
return false;
}
return scripts.every(
(script) => selectedScripts.some((selected) => selected.id === script.id),
);
}
public isAnyScriptSelected(category: ICategory): boolean {
const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) {
return false;
}
return selectedScripts.some((s) => category.includes(s.script));
}
public processChanges(action: CategorySelectionChangeCommand): void {
const scriptChanges = action.changes.reduce((changes, change) => {
changes.push(...this.collectScriptChanges(change));
return changes;
}, new Array<ScriptSelectionChange>());
this.scriptSelection.processChanges({
changes: scriptChanges,
});
}
private collectScriptChanges(change: CategorySelectionChange): ScriptSelectionChange[] {
const category = this.collection.getCategory(change.categoryId);
const scripts = category.getAllScriptsRecursively();
const scriptsChangesInCategory = scripts
.map((script): ScriptSelectionChange => ({
scriptId: script.id,
newStatus: {
...change.newStatus,
},
}));
return scriptsChangesInCategory;
}
}

View File

@@ -1,23 +0,0 @@
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { SelectedScript } from './SelectedScript';
export interface IReadOnlyUserSelection {
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>;
isSelected(scriptId: string): boolean;
areAllSelected(category: ICategory): boolean;
isAnySelected(category: ICategory): boolean;
}
export interface IUserSelection extends IReadOnlyUserSelection {
removeAllInCategory(categoryId: number): void;
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
removeSelectedScript(scriptId: string): void;
selectOnly(scripts: ReadonlyArray<IScript>): void;
selectAll(): void;
deselectAll(): void;
}

View File

@@ -0,0 +1,171 @@
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
import { ScriptSelection } from './ScriptSelection';
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
import { SelectedScript } from './SelectedScript';
import { UserSelectedScript } from './UserSelectedScript';
const DEBOUNCE_DELAY_IN_MS = 100;
export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeCommand>;
export class DebouncedScriptSelection implements ScriptSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: Repository<string, SelectedScript>;
public readonly processChanges: ScriptSelection['processChanges'];
constructor(
private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>,
debounce: DebounceFunction = batchedDebounce,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
for (const script of selectedScripts) {
this.scripts.addItem(script);
}
this.processChanges = debounce(
(batchedRequests: readonly ScriptSelectionChangeCommand[]) => {
const consolidatedChanges = batchedRequests.flatMap((request) => request.changes);
this.processScriptChanges(consolidatedChanges);
},
DEBOUNCE_DELAY_IN_MS,
);
}
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}
public get selectedScripts(): readonly SelectedScript[] {
return this.scripts.getItems();
}
public selectAll(): void {
const scriptsToSelect = this.collection
.getAllScripts()
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new UserSelectedScript(script, false));
if (scriptsToSelect.length === 0) {
return;
}
this.processChanges({
changes: scriptsToSelect.map((script): ScriptSelectionChange => ({
scriptId: script.id,
newStatus: {
isSelected: true,
isReverted: false,
},
})),
});
}
public deselectAll(): void {
if (this.scripts.length === 0) {
return;
}
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
this.processChanges({
changes: selectedScriptIds.map((scriptId): ScriptSelectionChange => ({
scriptId,
newStatus: {
isSelected: false,
},
})),
});
}
public selectOnly(scripts: readonly IScript[]): void {
if (scripts.length === 0) {
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
}
this.processChanges({
changes: [
...getScriptIdsToBeDeselected(this.scripts, scripts)
.map((scriptId): ScriptSelectionChange => ({
scriptId,
newStatus: {
isSelected: false,
},
})),
...getScriptIdsToBeSelected(this.scripts, scripts)
.map((scriptId): ScriptSelectionChange => ({
scriptId,
newStatus: {
isSelected: true,
isReverted: false,
},
})),
],
});
}
private processScriptChanges(changes: readonly ScriptSelectionChange[]): void {
let totalChanged = 0;
for (const change of changes) {
totalChanged += this.applyChange(change);
}
if (totalChanged > 0) {
this.changed.notify(this.scripts.getItems());
}
}
private applyChange(change: ScriptSelectionChange): number {
const script = this.collection.getScript(change.scriptId);
if (change.newStatus.isSelected) {
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
}
return this.removeScript(script.id);
}
private addOrUpdateScript(scriptId: string, revert: boolean): number {
const script = this.collection.getScript(scriptId);
const selectedScript = new UserSelectedScript(script, revert);
if (!this.scripts.exists(selectedScript.id)) {
this.scripts.addItem(selectedScript);
return 1;
}
const existingSelectedScript = this.scripts.getById(selectedScript.id);
if (equals(selectedScript, existingSelectedScript)) {
return 0;
}
this.scripts.addOrUpdateItem(selectedScript);
return 1;
}
private removeScript(scriptId: string): number {
if (!this.scripts.exists(scriptId)) {
return 0;
}
this.scripts.removeItem(scriptId);
return 1;
}
}
function getScriptIdsToBeSelected(
existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[],
): string[] {
return desiredScripts
.filter((script) => !existingItems.exists(script.id))
.map((script) => script.id);
}
function getScriptIdsToBeDeselected(
existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[],
): string[] {
return existingItems
.getItems()
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
.map((script) => script.id);
}
function equals(a: SelectedScript, b: SelectedScript): boolean {
return a.script.equals(b.script.id) && a.revert === b.revert;
}

View File

@@ -0,0 +1,17 @@
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IScript } from '@/domain/IScript';
import { SelectedScript } from './SelectedScript';
import { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
export interface ReadonlyScriptSelection {
readonly changed: IEventSource<readonly SelectedScript[]>;
readonly selectedScripts: readonly SelectedScript[];
isSelected(scriptId: string): boolean;
}
export interface ScriptSelection extends ReadonlyScriptSelection {
selectOnly(scripts: readonly IScript[]): void;
selectAll(): void;
deselectAll(): void;
processChanges(action: ScriptSelectionChangeCommand): void;
}

View File

@@ -0,0 +1,15 @@
export type ScriptSelectionStatus = {
readonly isSelected: true;
readonly isReverted: boolean;
} | {
readonly isSelected: false;
};
export interface ScriptSelectionChange {
readonly scriptId: string;
readonly newStatus: ScriptSelectionStatus;
}
export interface ScriptSelectionChangeCommand {
readonly changes: ReadonlyArray<ScriptSelectionChange>;
}

View File

@@ -0,0 +1,9 @@
import { IEntity } from '@/infrastructure/Entity/IEntity';
import { IScript } from '@/domain/IScript';
type ScriptId = IScript['id'];
export interface SelectedScript extends IEntity<ScriptId> {
readonly script: IScript;
readonly revert: boolean;
}

View File

@@ -1,14 +1,17 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from '@/domain/IScript';
import { SelectedScript } from './SelectedScript';
export class SelectedScript extends BaseEntity<string> {
type SelectedScriptId = SelectedScript['id'];
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
constructor(
public readonly script: IScript,
public readonly revert: boolean,
) {
super(script.id);
if (revert && !script.canRevert()) {
throw new Error('cannot revert an irreversible script');
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
}
}
}

View File

@@ -1,167 +1,12 @@
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IRepository } from '@/infrastructure/Repository/IRepository';
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { IUserSelection } from './IUserSelection';
import { SelectedScript } from './SelectedScript';
import { CategorySelection, ReadonlyCategorySelection } from './Category/CategorySelection';
import { ReadonlyScriptSelection, ScriptSelection } from './Script/ScriptSelection';
export class UserSelection implements IUserSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript>;
constructor(
private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
for (const script of selectedScripts) {
this.scripts.addItem(script);
}
}
public areAllSelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
const scripts = category.getAllScriptsRecursively();
if (this.selectedScripts.length < scripts.length) {
return false;
}
return scripts.every(
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
);
}
public isAnySelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
return this.selectedScripts.some((s) => category.includes(s.script));
}
public removeAllInCategory(categoryId: number): void {
const category = this.collection.findCategory(categoryId);
const scriptsToRemove = category.getAllScriptsRecursively()
.filter((script) => this.scripts.exists(script.id));
if (!scriptsToRemove.length) {
return;
}
for (const script of scriptsToRemove) {
this.scripts.removeItem(script.id);
}
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
const scriptsToAddOrUpdate = this.collection
.findCategory(categoryId)
.getAllScriptsRecursively()
.filter(
(script) => !this.scripts.exists(script.id)
|| this.scripts.getById(script.id).revert !== revert,
)
.map((script) => new SelectedScript(script, revert));
if (!scriptsToAddOrUpdate.length) {
return;
}
for (const script of scriptsToAddOrUpdate) {
this.scripts.addOrUpdateItem(script);
}
this.changed.notify(this.scripts.getItems());
}
public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
if (!script) {
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
}
const selectedScript = new SelectedScript(script, revert);
this.scripts.addItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public removeSelectedScript(scriptId: string): void {
this.scripts.removeItem(scriptId);
this.changed.notify(this.scripts.getItems());
}
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}
/** Get users scripts based on his/her selections */
public get selectedScripts(): ReadonlyArray<SelectedScript> {
return this.scripts.getItems();
}
public selectAll(): void {
const scriptsToSelect = this.collection
.getAllScripts()
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
if (scriptsToSelect.length === 0) {
return;
}
for (const script of scriptsToSelect) {
this.scripts.addItem(script);
}
this.changed.notify(this.scripts.getItems());
}
public deselectAll(): void {
if (this.scripts.length === 0) {
return;
}
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
for (const scriptId of selectedScriptIds) {
this.scripts.removeItem(scriptId);
}
this.changed.notify([]);
}
public selectOnly(scripts: readonly IScript[]): void {
if (!scripts || scripts.length === 0) {
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
}
let totalChanged = 0;
totalChanged += this.unselectMissingWithoutNotifying(scripts);
totalChanged += this.selectNewWithoutNotifying(scripts);
if (totalChanged > 0) {
this.changed.notify(this.scripts.getItems());
}
}
private unselectMissingWithoutNotifying(scripts: readonly IScript[]): number {
if (this.scripts.length === 0 || scripts.length === 0) {
return 0;
}
const existingItems = this.scripts.getItems();
const missingIds = existingItems
.filter((existing) => !scripts.some((script) => existing.id === script.id))
.map((script) => script.id);
for (const id of missingIds) {
this.scripts.removeItem(id);
}
return missingIds.length;
}
private selectNewWithoutNotifying(scripts: readonly IScript[]): number {
const unselectedScripts = scripts
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
for (const newScript of unselectedScripts) {
this.scripts.addItem(newScript);
}
return unselectedScripts.length;
}
export interface ReadonlyUserSelection {
readonly categories: ReadonlyCategorySelection;
readonly scripts: ReadonlyScriptSelection;
}
export interface UserSelection extends ReadonlyUserSelection {
readonly categories: CategorySelection;
readonly scripts: ScriptSelection;
}

View File

@@ -0,0 +1,39 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategorySelection } from './Category/CategorySelection';
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
import { ScriptSelection } from './Script/ScriptSelection';
import { UserSelection } from './UserSelection';
import { SelectedScript } from './Script/SelectedScript';
export class UserSelectionFacade implements UserSelection {
public readonly categories: CategorySelection;
public readonly scripts: ScriptSelection;
constructor(
collection: ICategoryCollection,
selectedScripts: readonly SelectedScript[],
scriptsFactory = DefaultScriptsFactory,
categoriesFactory = DefaultCategoriesFactory,
) {
this.scripts = scriptsFactory(collection, selectedScripts);
this.categories = categoriesFactory(this.scripts, collection);
}
}
export type ScriptsFactory = (
...params: ConstructorParameters<typeof DebouncedScriptSelection>
) => ScriptSelection;
const DefaultScriptsFactory: ScriptsFactory = (
...params
) => new DebouncedScriptSelection(...params);
export type CategoriesFactory = (
...params: ConstructorParameters<typeof ScriptToCategorySelectionMapper>
) => CategorySelection;
const DefaultCategoriesFactory: CategoriesFactory = (
...params
) => new ScriptToCategorySelectionMapper(...params);

View File

@@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [
];
function validateCollectionsData(collections: readonly CollectionData[]) {
if (!collections?.length) {
if (!collections.length) {
throw new Error('missing collections');
}
if (collections.some((collection) => !collection)) {
throw new Error('missing collection provided');
}
}

View File

@@ -28,10 +28,7 @@ export function parseCategoryCollection(
}
function validate(content: CollectionData): void {
if (!content) {
throw new Error('missing content');
}
if (!content.actions || content.actions.length <= 0) {
if (!content.actions.length) {
throw new Error('content does not define any action');
}
}

View File

@@ -1,5 +1,5 @@
import type {
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
CategoryData, ScriptData, CategoryOrScriptData,
} from '@/application/collections/';
import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category';
@@ -16,7 +16,6 @@ export function parseCategory(
context: ICategoryCollectionParseContext,
factory: CategoryFactoryType = CategoryFactory,
): Category {
if (!context) { throw new Error('missing context'); }
return parseCategoryRecursively({
categoryData: category,
context,
@@ -30,8 +29,8 @@ interface ICategoryParseContext {
readonly factory: CategoryFactoryType,
readonly parentCategory?: CategoryData,
}
// eslint-disable-next-line consistent-return
function parseCategoryRecursively(context: ICategoryParseContext): Category {
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
ensureValidCategory(context.categoryData, context.parentCategory);
const children: ICategoryChildren = {
subCategories: new Array<Category>(),
@@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category {
/* scripts: */ children.subScripts,
);
} catch (err) {
new NodeValidator({
return new NodeValidator({
type: NodeType.Category,
selfNode: context.categoryData,
parentNode: context.parentCategory,
@@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa
.assertDefined(category)
.assertValidName(category.category)
.assert(
() => category.children && category.children.length > 0,
() => category.children.length > 0,
`"${category.category}" has no children.`,
);
}
@@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) {
validator.assertDefined(context.nodeData);
if (isCategory(context.nodeData)) {
const subCategory = parseCategoryRecursively({
categoryData: context.nodeData as CategoryData,
categoryData: context.nodeData,
context: context.context,
factory: context.factory,
parentCategory: context.parent,
});
context.children.subCategories.push(subCategory);
} else if (isScript(context.nodeData)) {
const script = parseScript(context.nodeData as ScriptData, context.context);
const script = parseScript(context.nodeData, context.context);
context.children.subScripts.push(script);
} else {
validator.throw('Node is neither a category or a script.');
@@ -109,19 +108,18 @@ function parseNode(context: INodeParseContext) {
}
function isScript(data: CategoryOrScriptData): data is ScriptData {
const holder = (data as InstructionHolder);
return hasCode(holder) || hasCall(holder);
return hasCode(data) || hasCall(data);
}
function isCategory(data: CategoryOrScriptData): data is CategoryData {
return hasProperty(data, 'category');
}
function hasCode(data: InstructionHolder): boolean {
function hasCode(data: unknown): boolean {
return hasProperty(data, 'code');
}
function hasCall(data: InstructionHolder) {
function hasCall(data: unknown) {
return hasProperty(data, 'call');
}

View File

@@ -1,9 +1,6 @@
import type { DocumentableData, DocumentationData } from '@/application/collections/';
export function parseDocs(documentable: DocumentableData): readonly string[] {
if (!documentable) {
throw new Error('missing documentable');
}
const { docs } = documentable;
if (!docs) {
return [];

View File

@@ -32,7 +32,7 @@ export class NodeValidator {
return this;
}
public throw(errorMessage: string) {
public throw(errorMessage: string): never {
throw new NodeDataError(errorMessage, this.context);
}
}

View File

@@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) {
if (!scripting) { throw new Error('missing scripting'); }
this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData, this.syntax);
this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax);
}
}

View File

@@ -15,19 +15,10 @@ export class Expression implements IExpression {
public readonly evaluator: ExpressionEvaluator,
parameters?: IReadOnlyFunctionParameterCollection,
) {
if (!position) {
throw new Error('missing position');
}
if (!evaluator) {
throw new Error('missing evaluator');
}
this.parameters = parameters ?? new FunctionParameterCollection();
}
public evaluate(context: IExpressionEvaluationContext): string {
if (!context) {
throw new Error('missing context');
}
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
const args = filterUnusedArguments(this.parameters, context.args);
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);

View File

@@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
public readonly args: IReadOnlyFunctionCallArgumentCollection,
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
) {
if (!args) {
throw new Error('missing args, send empty collection instead.');
}
}
}

View File

@@ -0,0 +1,16 @@
import { ExpressionPosition } from './ExpressionPosition';
export function createPositionFromRegexFullMatch(
match: RegExpMatchArray,
): ExpressionPosition {
const startPos = match.index;
if (startPos === undefined) {
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
}
const fullMatch = match[0];
if (!fullMatch.length) {
throw new Error(`Regex match is empty: ${JSON.stringify(match)}`);
}
const endPos = startPos + fullMatch.length;
return new ExpressionPosition(startPos, endPos);
}

View File

@@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
) { }
public compileExpressions(
code: string | undefined,
code: string,
args: IReadOnlyFunctionCallArgumentCollection,
): string {
if (!args) {
throw new Error('missing args, send empty collection instead.');
}
if (!code) {
return code;
return '';
}
const context = new ExpressionEvaluationContext(args);
const compiledCode = compileRecursively(code, context, this.extractor);
@@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
providedArgs: IReadOnlyFunctionCallArgumentCollection,
): void {
const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) {
if (!usedParameterNames.length) {
return;
}
const notProvidedParameters = usedParameterNames

View File

@@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
export interface IExpressionsCompiler {
compileExpressions(
code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection): string;
code: string,
args: IReadOnlyFunctionCallArgumentCollection,
): string;
}

View File

@@ -10,12 +10,9 @@ const Parsers = [
export class CompositeExpressionParser implements IExpressionParser {
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
if (!leafs) {
if (!leafs.length) {
throw new Error('missing leafs');
}
if (leafs.some((leaf) => !leaf)) {
throw new Error('missing leaf');
}
}
public findExpressions(code: string): IExpression[] {

View File

@@ -14,45 +14,44 @@ export class ExpressionRegexBuilder {
.addRawRegex('\\s+');
}
public matchPipeline() {
public captureOptionalPipeline() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('(\\|\\s*.+?)?');
.addRawRegex('((?:\\|\\s*\\b[a-zA-Z]+\\b\\s*)*)');
}
public matchUntilFirstWhitespace() {
public captureUntilWhitespaceOrPipe() {
return this
.addRawRegex('([^|\\s]+)');
}
public matchMultilineAnythingExceptSurroundingWhitespaces() {
public captureMultilineAnythingExceptSurroundingWhitespaces() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('([\\S\\s]+?)')
.expectZeroOrMoreWhitespaces();
.expectOptionalWhitespaces()
.addRawRegex('([\\s\\S]*\\S)')
.expectOptionalWhitespaces();
}
public expectExpressionStart() {
return this
.expectCharacters('{{')
.expectZeroOrMoreWhitespaces();
.expectOptionalWhitespaces();
}
public expectExpressionEnd() {
return this
.expectZeroOrMoreWhitespaces()
.expectOptionalWhitespaces()
.expectCharacters('}}');
}
public expectOptionalWhitespaces() {
return this
.addRawRegex('\\s*');
}
public buildRegExp(): RegExp {
return new RegExp(this.parts.join(''), 'g');
}
private expectZeroOrMoreWhitespaces() {
return this
.addRawRegex('\\s*');
}
private addRawRegex(regex: string) {
this.parts.push(regex);
return this;

View File

@@ -1,9 +1,9 @@
import { IExpressionParser } from '../IExpressionParser';
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
import { IExpression } from '../../Expression/IExpression';
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp;
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
const matches = code.matchAll(this.regex);
for (const match of matches) {
const primitiveExpression = this.buildExpression(match);
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
const parameters = createParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression;
@@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser {
}
}
function createPosition(match: RegExpMatchArray): ExpressionPosition {
const startPos = match.index;
const endPos = startPos + match[0].length;
return new ExpressionPosition(startPos, endPos);
}
function createParameters(
expression: IPrimitiveExpression,
): FunctionParameterCollection {

View File

@@ -28,7 +28,7 @@ function hasLines(text: string) {
*/
function inlineComments(code: string): string {
const makeInlineComment = (comment: string) => {
const value = comment?.trim();
const value = comment.trim();
if (!value) {
return '<##>';
}

View File

@@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
if (!pipes) {
throw new Error('missing pipes');
}
if (pipes.some((pipe) => !pipe)) {
throw new Error('missing pipe in list');
}
for (const pipe of pipes) {
this.registerPipe(pipe);
}
@@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory {
public get(pipeName: string): IPipe {
validatePipeName(pipeName);
if (!this.pipes.has(pipeName)) {
const pipe = this.pipes.get(pipeName);
if (!pipe) {
throw new Error(`Unknown pipe: "${pipeName}"`);
}
return this.pipes.get(pipeName);
return pipe;
}
private registerPipe(pipe: IPipe): void {

View File

@@ -6,8 +6,9 @@ export class ParameterSubstitutionParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder()
.expectExpressionStart()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: Parameter name
.matchPipeline() // Second match: Pipeline
.captureUntilWhitespaceOrPipe() // First capture: Parameter name
.expectOptionalWhitespaces()
.captureOptionalPipeline() // Second capture: Pipeline
.expectExpressionEnd()
.buildRegExp();

View File

@@ -1,59 +1,220 @@
// eslint-disable-next-line max-classes-per-file
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { IExpression } from '../Expression/IExpression';
import { ExpressionPosition } from '../Expression/ExpressionPosition';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
export class WithParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder()
// {{ with $parameterName }}
.expectExpressionStart()
.expectCharacters('with')
.expectOneOrMoreWhitespaces()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: parameter name
.expectExpressionEnd()
// ...
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
// {{ end }}
.expectExpressionStart()
.expectCharacters('end')
.expectExpressionEnd()
.buildRegExp();
export class WithParser implements IExpressionParser {
public findExpressions(code: string): IExpression[] {
if (!code) {
throw new Error('missing code');
}
return parseWithExpressions(code);
}
}
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
const scopeText = match[2];
enum WithStatementType {
Start,
End,
ContextVariable,
}
type WithStatement = {
readonly type: WithStatementType.Start;
readonly parameterName: string;
readonly position: ExpressionPosition;
} | {
readonly type: WithStatementType.End;
readonly position: ExpressionPosition;
} | {
readonly type: WithStatementType.ContextVariable;
readonly position: ExpressionPosition;
readonly pipeline: string | undefined;
};
function parseAllWithExpressions(
input: string,
): WithStatement[] {
const expressions = new Array<WithStatement>();
for (const match of input.matchAll(WithStatementStartRegEx)) {
expressions.push({
type: WithStatementType.Start,
parameterName: match[1],
position: createPositionFromRegexFullMatch(match),
});
}
for (const match of input.matchAll(WithStatementEndRegEx)) {
expressions.push({
type: WithStatementType.End,
position: createPositionFromRegexFullMatch(match),
});
}
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
expressions.push({
type: WithStatementType.ContextVariable,
position: createPositionFromRegexFullMatch(match),
pipeline: match[1],
});
}
return expressions;
}
class WithStatementBuilder {
private readonly contextVariables = new Array<{
readonly positionInScope: ExpressionPosition;
readonly pipeline: string | undefined;
}>();
public addContextVariable(
absolutePosition: ExpressionPosition,
pipeline: string | undefined,
): void {
const positionInScope = new ExpressionPosition(
absolutePosition.start - this.startExpressionPosition.end,
absolutePosition.end - this.startExpressionPosition.end,
);
this.contextVariables.push({
positionInScope,
pipeline,
});
}
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
const parameters = new FunctionParameterCollection();
parameters.addParameter(new FunctionParameter(this.parameterName, true));
const position = new ExpressionPosition(
this.startExpressionPosition.start,
endExpressionPosition.end,
);
const scope = input.substring(this.startExpressionPosition.end, endExpressionPosition.start);
return {
parameters: [new FunctionParameter(parameterName, true)],
evaluator: (context) => {
const argumentValue = context.args.hasArgument(parameterName)
? context.args.getArgument(parameterName).argumentValue
parameters,
position,
evaluate: (context) => {
const argumentValue = context.args.hasArgument(this.parameterName)
? context.args.getArgument(this.parameterName).argumentValue
: undefined;
if (!argumentValue) {
return '';
}
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
const substitutedScope = this.substituteContextVariables(scope, (pipeline) => {
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
});
return substitutedScope;
},
};
}
constructor(
private readonly startExpressionPosition: ExpressionPosition,
private readonly parameterName: string,
) {
}
private substituteContextVariables(
scope: string,
substituter: (pipeline?: string) => string,
): string {
if (!this.contextVariables.length) {
return scope;
}
let substitutedScope = '';
let scopeSubstrIndex = 0;
for (const contextVariable of this.contextVariables) {
substitutedScope += scope.substring(scopeSubstrIndex, contextVariable.positionInScope.start);
substitutedScope += substituter(contextVariable.pipeline);
scopeSubstrIndex = contextVariable.positionInScope.end;
}
substitutedScope += scope.substring(scopeSubstrIndex, scope.length);
return substitutedScope;
}
}
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
function buildErrorContext(code: string, statements: readonly WithStatement[]): string {
const formattedStatements = statements.map((s) => `- [${s.position.start}, ${s.position.end}] ${WithStatementType[s.type]}`).join('\n');
return [
'Code:', '---', code, '---',
'nStatements:', '---', formattedStatements, '---',
].join('\n');
}
function parseWithExpressions(input: string): IExpression[] {
const allStatements = parseAllWithExpressions(input);
const sortedStatements = allStatements
.slice()
.sort((a, b) => b.position.start - a.position.start);
const expressions = new Array<IExpression>();
const builders = new Array<WithStatementBuilder>();
const throwWithContext = (message: string): never => {
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
};
while (sortedStatements.length > 0) {
const statement = sortedStatements.pop();
if (!statement) {
break;
}
switch (statement.type) { // eslint-disable-line default-case
case WithStatementType.Start:
builders.push(new WithStatementBuilder(
statement.position,
statement.parameterName,
));
break;
case WithStatementType.ContextVariable:
if (builders.length === 0) {
throwWithContext('Context variable before `with` statement.');
}
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
break;
case WithStatementType.End: {
const builder = builders.pop();
if (!builder) {
throwWithContext('Redundant `end` statement, missing `with`?');
break;
}
expressions.push(builder.buildExpression(statement.position, input));
break;
}
}
}
if (builders.length > 0) {
throwWithContext('Missing `end` statement, forgot `{{ end }}?');
}
return expressions;
}
const ContextVariableWithPipelineRegEx = new ExpressionRegexBuilder()
// {{ . | pipeName }}
.expectExpressionStart()
.expectCharacters('.')
.matchPipeline() // First match: pipeline
.expectOptionalWhitespaces()
.captureOptionalPipeline() // First capture: pipeline
.expectExpressionEnd()
.buildRegExp();
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
// but instead letting the pipeline compiler to fail on those.
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
return replacer(match1);
});
}
const WithStatementStartRegEx = new ExpressionRegexBuilder()
// {{ with $parameterName }}
.expectExpressionStart()
.expectCharacters('with')
.expectOneOrMoreWhitespaces()
.expectCharacters('$')
.captureUntilWhitespaceOrPipe() // First capture: parameter name
.expectExpressionEnd()
.expectOptionalWhitespaces()
.buildRegExp();
const WithStatementEndRegEx = new ExpressionRegexBuilder()
// {{ end }}
.expectOptionalWhitespaces()
.expectExpressionStart()
.expectCharacters('end')
.expectOptionalWhitespaces()
.expectExpressionEnd()
.buildRegExp();

View File

@@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void {
if (!argument) {
throw new Error('missing argument');
}
if (this.hasArgument(argument.parameterName)) {
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
}

View File

@@ -3,18 +3,22 @@ import { CodeSegmentMerger } from './CodeSegmentMerger';
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
if (!codeSegments?.length) {
if (!codeSegments.length) {
throw new Error('missing segments');
}
return {
code: joinCodeParts(codeSegments.map((f) => f.code)),
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
revertCode: joinCodeParts(
codeSegments
.map((f) => f.revertCode)
.filter((code): code is string => Boolean(code)),
),
};
}
}
function joinCodeParts(codeSegments: readonly string[]): string {
return codeSegments
.filter((segment) => segment?.length > 0)
.filter((segment) => segment.length > 0)
.join('\n');
}

View File

@@ -21,9 +21,7 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
calls: readonly FunctionCall[],
functions: ISharedFunctionCollection,
): CompiledCode {
if (!functions) { throw new Error('missing functions'); }
if (!calls?.length) { throw new Error('missing calls'); }
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
if (!calls.length) { throw new Error('missing calls'); }
const context: FunctionCallCompilationContext = {
allFunctions: functions,
rootCallSequence: calls,

View File

@@ -1,6 +1,6 @@
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
@@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
}
public canCompile(func: ISharedFunction): boolean {
return func.body.code !== undefined;
return func.body.type === FunctionBodyType.Code;
}
public compileFunction(
calledFunction: ISharedFunction,
callToFunction: FunctionCall,
): CompiledCode[] {
if (calledFunction.body.type !== FunctionBodyType.Code) {
throw new Error([
'Unexpected function body type.',
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
'Function:',
`\t${JSON.stringify(callToFunction)}`,
].join('\n'));
}
const { code } = calledFunction.body;
const { args } = callToFunction;
return [
{
code: this.expressionsCompiler.compileExpressions(code.execute, args),
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
revertCode: (() => {
if (!code.revert) {
return undefined;
}
return this.expressionsCompiler.compileExpressions(code.revert, args);
})(),
},
];
}

View File

@@ -1,4 +1,4 @@
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
@@ -13,7 +13,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
}
public canCompile(func: ISharedFunction): boolean {
return func.body.calls !== undefined;
return func.body.type === FunctionBodyType.Calls;
}
public compileFunction(
@@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
callToFunction: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
const nestedCalls = calledFunction.body.calls;
const nestedCalls = (calledFunction.body as CallFunctionBody).calls;
return nestedCalls.map((nestedCall) => {
try {
const compiledParentCall = this.argumentCompiler

View File

@@ -5,9 +5,6 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { ParsedFunctionCall } from './ParsedFunctionCall';
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
if (calls === undefined) {
throw new Error('missing call data');
}
const sequence = getCallSequence(calls);
return sequence.map((call) => parseFunctionCall(call));
}
@@ -19,22 +16,21 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
if (calls instanceof Array) {
return calls as FunctionCallData[];
}
return [calls as FunctionCallData];
const singleCall = calls;
return [singleCall];
}
function parseFunctionCall(call: FunctionCallData): FunctionCall {
if (!call) {
throw new Error('missing call data');
}
const callArgs = parseArgs(call.parameters);
return new ParsedFunctionCall(call.function, callArgs);
}
function parseArgs(
parameters: FunctionCallParametersData,
parameters: FunctionCallParametersData | undefined,
): FunctionCallArgumentCollection {
return Object.keys(parameters || {})
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
const parametersMap = parameters ?? {};
return Object.keys(parametersMap)
.map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName]))
.reduce((args, arg) => {
args.addArgument(arg);
return args;

View File

@@ -9,8 +9,5 @@ export class ParsedFunctionCall implements FunctionCall {
if (!functionName) {
throw new Error('missing function name in function call');
}
if (!args) {
throw new Error('missing args');
}
}
}

View File

@@ -4,15 +4,21 @@ import { FunctionCall } from './Call/FunctionCall';
export interface ISharedFunction {
readonly name: string;
readonly parameters: IReadOnlyFunctionParameterCollection;
readonly body: ISharedFunctionBody;
readonly body: SharedFunctionBody;
}
export interface ISharedFunctionBody {
readonly type: FunctionBodyType;
readonly code: IFunctionCode | undefined;
readonly calls: readonly FunctionCall[] | undefined;
export interface CallFunctionBody {
readonly type: FunctionBodyType.Calls,
readonly calls: readonly FunctionCall[],
}
export interface CodeFunctionBody {
readonly type: FunctionBodyType.Code;
readonly code: IFunctionCode,
}
export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody;
export enum FunctionBodyType {
Code,
Calls,

View File

@@ -18,9 +18,6 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
}
private ensureValidParameter(parameter: IFunctionParameter) {
if (!parameter) {
throw new Error('missing parameter');
}
if (this.includesName(parameter.name)) {
throw new Error(`duplicate parameter name: "${parameter.name}"`);
}

View File

@@ -1,7 +1,7 @@
import { FunctionCall } from './Call/FunctionCall';
import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody,
} from './ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
@@ -10,7 +10,7 @@ export function createCallerFunction(
parameters: IReadOnlyFunctionParameterCollection,
callSequence: readonly FunctionCall[],
): ISharedFunction {
if (!callSequence || !callSequence.length) {
if (!callSequence.length) {
throw new Error(`missing call sequence in function "${name}"`);
}
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
@@ -33,7 +33,7 @@ export function createFunctionWithInlineCode(
}
class SharedFunction implements ISharedFunction {
public readonly body: ISharedFunctionBody;
public readonly body: SharedFunctionBody;
constructor(
public readonly name: string,
@@ -42,11 +42,22 @@ class SharedFunction implements ISharedFunction {
bodyType: FunctionBodyType,
) {
if (!name) { throw new Error('missing function name'); }
if (!parameters) { throw new Error('missing parameters'); }
this.body = {
type: bodyType,
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
};
switch (bodyType) {
case FunctionBodyType.Code:
this.body = {
type: FunctionBodyType.Code,
code: content as IFunctionCode,
};
break;
case FunctionBodyType.Calls:
this.body = {
type: FunctionBodyType.Calls,
calls: content as readonly FunctionCall[],
};
break;
default:
throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`);
}
}
}

View File

@@ -5,7 +5,6 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
private readonly functionsByName = new Map<string, ISharedFunction>();
public addFunction(func: ISharedFunction): void {
if (!func) { throw new Error('missing function'); }
if (this.has(func.name)) {
throw new Error(`function with name ${func.name} already exists`);
}

View File

@@ -1,4 +1,6 @@
import type { FunctionData, InstructionHolder } from '@/application/collections/';
import type {
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
} from '@/application/collections/';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
@@ -23,9 +25,8 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
): ISharedFunctionCollection {
if (!syntax) { throw new Error('missing syntax'); }
const collection = new SharedFunctionCollection();
if (!functions || !functions.length) {
if (!functions.length) {
return collection;
}
ensureValidFunctions(functions);
@@ -55,16 +56,18 @@ function parseFunction(
}
function validateCode(
data: FunctionData,
data: CodeFunctionData,
syntax: ILanguageSyntax,
validator: ICodeValidator,
): void {
[data.code, data.revertCode].forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
[data.code, data.revertCode]
.filter((code): code is string => Boolean(code))
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
}
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
@@ -85,19 +88,18 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti
}, new FunctionParameterCollection());
}
function hasCode(data: FunctionData): boolean {
return Boolean(data.code);
function hasCode(data: FunctionData): data is CodeFunctionData {
return (data as CodeInstruction).code !== undefined;
}
function hasCall(data: FunctionData): boolean {
return Boolean(data.call);
function hasCall(data: FunctionData): data is CallFunctionData {
return (data as CallInstruction).call !== undefined;
}
function ensureValidFunctions(functions: readonly FunctionData[]) {
ensureNoUndefinedItem(functions);
ensureNoDuplicatesInFunctionNames(functions);
ensureNoDuplicateCode(functions);
ensureEitherCallOrCodeIsDefined(functions);
ensureNoDuplicateCode(functions);
ensureExpectedParametersType(functions);
}
@@ -105,7 +107,7 @@ function printList(list: readonly string[]): string {
return `"${list.join('","')}"`;
}
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
// Ensure functions do not define both call and code
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
if (withBothCallAndCode.length) {
@@ -132,7 +134,7 @@ function isArrayOfObjects(value: unknown): boolean {
&& value.every((item) => typeof item === 'object');
}
function printNames(holders: readonly InstructionHolder[]) {
function printNames(holders: readonly FunctionData[]) {
return printList(holders.map((holder) => holder.name));
}
@@ -144,22 +146,19 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
}
}
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
if (functions.some((func) => !func)) {
throw new Error('some functions are undefined');
}
}
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
const duplicateCodes = getDuplicates(functions
const callFunctions = functions
.filter((func) => hasCode(func))
.map((func) => func as CodeFunctionData);
const duplicateCodes = getDuplicates(callFunctions
.map((func) => func.code)
.filter((code) => code));
if (duplicateCodes.length > 0) {
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
}
const duplicateRevertCodes = getDuplicates(functions
.filter((func) => func.revertCode)
.map((func) => func.revertCode));
const duplicateRevertCodes = getDuplicates(callFunctions
.map((func) => func.revertCode)
.filter((code): code is string => Boolean(code)));
if (duplicateRevertCodes.length > 0) {
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
}

View File

@@ -1,4 +1,4 @@
import type { FunctionData, ScriptData } from '@/application/collections/';
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
@@ -18,27 +18,24 @@ export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection;
constructor(
functions: readonly FunctionData[] | undefined,
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
) {
if (!syntax) { throw new Error('missing syntax'); }
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
}
public canCompile(script: ScriptData): boolean {
if (!script) { throw new Error('missing script'); }
if (!script.call) {
return false;
}
return true;
return hasCall(script);
}
public compile(script: ScriptData): IScriptCode {
if (!script) { throw new Error('missing script'); }
try {
if (!hasCall(script)) {
throw new Error('Script does include any calls.');
}
const calls = parseFunctionCalls(script.call);
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
validateCompiledCode(compiledCode, this.codeValidator);
@@ -53,7 +50,17 @@ export class ScriptCompiler implements IScriptCompiler {
}
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
[compiledCode.code, compiledCode.revertCode].forEach(
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
);
[compiledCode.code, compiledCode.revertCode]
.filter((code): code is string => Boolean(code))
.map((code) => code as string)
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines()],
),
);
}
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
return (data as CallInstruction).call !== undefined;
}

View File

@@ -1,4 +1,4 @@
import type { ScriptData } from '@/application/collections/';
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { Script } from '@/domain/Script';
@@ -14,7 +14,6 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte
import { CodeValidator } from './Validation/CodeValidator';
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
// eslint-disable-next-line consistent-return
export function parseScript(
data: ScriptData,
context: ICategoryCollectionParseContext,
@@ -24,7 +23,6 @@ export function parseScript(
): Script {
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
validateScript(data, validator);
if (!context) { throw new Error('missing context'); }
try {
const script = scriptFactory(
/* name: */ data.name,
@@ -34,12 +32,12 @@ export function parseScript(
);
return script;
} catch (err) {
validator.throw(err.message);
return validator.throw(err.message);
}
}
function parseLevel(
level: string,
level: string | undefined,
parser: IEnumParser<RecommendationLevel>,
): RecommendationLevel | undefined {
if (!level) {
@@ -56,39 +54,45 @@ function parseCode(
if (context.compiler.canCompile(script)) {
return context.compiler.compile(script);
}
const code = new ScriptCode(script.code, script.revertCode);
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
return code;
}
function validateHardcodedCodeWithoutCalls(
scriptCode: ScriptCode,
codeValidator: ICodeValidator,
validator: ICodeValidator,
syntax: ILanguageSyntax,
) {
[scriptCode.execute, scriptCode.revert].forEach(
(code) => codeValidator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
[scriptCode.execute, scriptCode.revert]
.filter((code): code is string => Boolean(code))
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
}
function validateScript(script: ScriptData, validator: NodeValidator) {
function validateScript(
script: ScriptData,
validator: NodeValidator,
): asserts script is NonNullable<ScriptData> {
validator
.assertDefined(script)
.assertValidName(script.name)
.assert(
() => Boolean(script.code || script.call),
'Must define either "call" or "code".',
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
'Neither "call" or "code" is defined.',
)
.assert(
() => !(script.code && script.call),
'Cannot define both "call" and "code".',
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
'Both "call" and "code" are defined.',
)
.assert(
() => !(script.revertCode && script.call),
'Cannot define "revertCode" if "call" is defined.',
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
'Both "call" and "revertCode" are defined.',
);
}

Some files were not shown because too many files have changed in this diff Show More