diff --git a/.gitignore b/.gitignore
index d6c6d41e3ca..9dd73547646 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,3 +106,10 @@ tags.lock
tags.temp
.stylelintcache
.solargraph.yml
+
+# Vite Ruby
+/public/vite*
+# Vite uses dotenv and suggests to ignore local-only env files. See
+# https://vitejs.dev/guide/env-and-mode.html#env-files
+*.local
+
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 145db0db871..c7b47be48fa 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -1170,21 +1170,6 @@ Layout/ArgumentAlignment:
- 'ee/spec/requests/groups/two_factor_auths_controller_spec.rb'
- 'ee/spec/requests/smartcard_controller_spec.rb'
- 'ee/spec/requests/users/identity_verification_controller_spec.rb'
- - 'ee/spec/services/ee/merge_requests/refresh_service_spec.rb'
- - 'ee/spec/services/ee/merge_requests/update_assignees_service_spec.rb'
- - 'ee/spec/services/ee/merge_requests/update_reviewers_service_spec.rb'
- - 'ee/spec/services/ee/notification_service_spec.rb'
- - 'ee/spec/services/ee/users/migrate_records_to_ghost_user_service_spec.rb'
- - 'ee/spec/services/ee/vulnerability_feedback_module/update_service_spec.rb'
- - 'ee/spec/services/elastic/process_bookkeeping_service_spec.rb'
- - 'ee/spec/services/epics/issue_promote_service_spec.rb'
- - 'ee/spec/services/geo/blob_upload_service_spec.rb'
- - 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/hashed_storage_attachments_migration_service_spec.rb'
- - 'ee/spec/services/geo/registry_consistency_service_spec.rb'
- - 'ee/spec/services/geo/replication_toggle_request_service_spec.rb'
- - 'ee/spec/services/geo/repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/wiki_sync_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/reconciliations/check_seat_usage_alerts_eligibility_service_spec.rb'
- 'ee/spec/services/groups/compliance_report_csv_service_spec.rb'
- 'ee/spec/services/groups/mark_for_deletion_service_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 1858117ce4e..e00d02d4861 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-db234c87ebaff374e91734527bcf9ce2bff357d5
+69b25765133ae3c6b8fdd9eec85de30de607e389
diff --git a/Gemfile b/Gemfile
index 7acfec4a9db..a0c8aa7854b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -19,6 +19,8 @@ gem 'rails', '~> 7.0.6'
gem 'activerecord-gitlab', path: 'gems/activerecord-gitlab'
+gem 'vite_rails'
+
gem 'bootsnap', '~> 1.16.0', require: false
gem 'openssl', '~> 3.0'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 44b2c331b68..4286d1bc47c 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -125,6 +125,7 @@
{"name":"doorkeeper","version":"5.6.6","platform":"ruby","checksum":"2344e86c77770526efcda893b5217aa13d1c7eb1b40de840b58b19eb1ff757e0"},
{"name":"doorkeeper-openid_connect","version":"1.8.7","platform":"ruby","checksum":"71edaf33118deefe25674ba3f8280c32835f057351f70e9beb222c0fd6b8e786"},
{"name":"dotenv","version":"2.7.6","platform":"ruby","checksum":"2451ed5e8e43776d7a787e51d6f8903b98e446146c7ad143d5678cc2c409d547"},
+{"name":"dry-cli","version":"1.0.0","platform":"ruby","checksum":"28ead169f872954dd08910eb8ead59cf86cd18b4aab321e8eeefe945749569f0"},
{"name":"dry-core","version":"1.0.0","platform":"ruby","checksum":"7a92099870967f0d2c9997950608cb8bb622dafeea20b2fe1dd49e9ba1d0f305"},
{"name":"dry-inflector","version":"1.0.0","platform":"ruby","checksum":"6ad22361ca2d6f3f001ae3037ffcfea01163f644280d13a9195d3c3a94dd1626"},
{"name":"dry-logic","version":"1.5.0","platform":"ruby","checksum":"99ed2180f1970c3d8247004f277a01dffbe8e82cf6680de9c7209312d86cd416"},
@@ -675,6 +676,8 @@
{"name":"version_sorter","version":"2.3.0","platform":"ruby","checksum":"2147f2a1a3804fbb8f60d268b7d7c1ec717e6dd727ffe2c165b4e05e82efe1da"},
{"name":"view_component","version":"3.2.0","platform":"ruby","checksum":"1dfaa85e13b5393f30b60bd3a03348b5298240a13137985d71eb2b8cc94c4c22"},
{"name":"virtus","version":"2.0.0","platform":"ruby","checksum":"8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2"},
+{"name":"vite_rails","version":"3.0.15","platform":"ruby","checksum":"b8ec528aedf7e24b54f222b449cd9250810ea2456d5f8dd4ef87f06b475cf860"},
+{"name":"vite_ruby","version":"3.3.4","platform":"ruby","checksum":"025e438385a6dc2320c8c148dff453f5bb1d4f056ce69c3386f47d4c388ad80c"},
{"name":"vmstat","version":"2.3.0","platform":"ruby","checksum":"ab5446a3e3bd0a9cdb9d9ac69a0bbd119c4f161d945a0846a519dd7018af656d"},
{"name":"warden","version":"1.2.9","platform":"ruby","checksum":"46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0"},
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 9ec61c29cd2..860df258e7e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -459,6 +459,7 @@ GEM
doorkeeper (>= 5.5, < 5.7)
jwt (>= 2.5)
dotenv (2.7.6)
+ dry-cli (1.0.0)
dry-core (1.0.0)
concurrent-ruby (~> 1.0)
zeitwerk (~> 2.6)
@@ -1672,6 +1673,13 @@ GEM
axiom-types (~> 0.1)
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
+ vite_rails (3.0.15)
+ railties (>= 5.1, < 8)
+ vite_ruby (~> 3.0, >= 3.2.2)
+ vite_ruby (3.3.4)
+ dry-cli (>= 0.7, < 2)
+ rack-proxy (~> 0.6, >= 0.6.1)
+ zeitwerk (~> 2.2)
vmstat (2.3.0)
warden (1.2.9)
rack (>= 2.0.9)
@@ -2029,6 +2037,7 @@ DEPENDENCIES
validates_hostname (~> 1.0.11)
version_sorter (~> 2.3)
view_component (~> 3.2.0)
+ vite_rails
vmstat (~> 2.3.0)
warning (~> 1.3.0)
webauthn (~> 3.0)
diff --git a/Rakefile b/Rakefile
index 4db94c50130..36176721e13 100755
--- a/Rakefile
+++ b/Rakefile
@@ -11,6 +11,10 @@ require File.expand_path('config/application', __dir__)
relative_url_conf = File.expand_path('config/initializers/relative_url', __dir__)
require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
+# This is the only way to change how vite_ruby works for rake tasks
+# See https://github.com/ElMassimo/vite_ruby/blob/vite_ruby%403.3.4/vite_ruby/lib/tasks/vite.rake#L58
+ENV['VITE_RUBY_SKIP_ASSETS_PRECOMPILE_EXTENSION'] = 'true'
+
Gitlab::Application.load_tasks
Knapsack.load_tasks if defined?(Knapsack)
diff --git a/app/assets/javascripts/entrypoints/jira_connect_app.js b/app/assets/javascripts/entrypoints/jira_connect_app.js
new file mode 100644
index 00000000000..90ad39ea487
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/jira_connect_app.js
@@ -0,0 +1 @@
+import '../jira_connect/subscriptions';
diff --git a/app/assets/javascripts/entrypoints/main.js b/app/assets/javascripts/entrypoints/main.js
new file mode 100644
index 00000000000..6d59e89cfd0
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/main.js
@@ -0,0 +1,6 @@
+import '../main';
+import { runModules } from '~/run_modules';
+
+const modules = import.meta.glob('../pages/**/index.js');
+
+runModules(modules, '../pages/');
diff --git a/app/assets/javascripts/entrypoints/main_ee.js b/app/assets/javascripts/entrypoints/main_ee.js
new file mode 100644
index 00000000000..4a83be6be94
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/main_ee.js
@@ -0,0 +1,5 @@
+import { runModules } from '~/run_modules';
+
+const modules = import.meta.glob('../../../../ee/app/assets/javascripts/pages/**/index.js');
+
+runModules(modules, '../../../../ee/app/assets/javascripts/pages/');
diff --git a/app/assets/javascripts/entrypoints/main_jh.js b/app/assets/javascripts/entrypoints/main_jh.js
new file mode 100644
index 00000000000..92a42a9ac70
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/main_jh.js
@@ -0,0 +1,5 @@
+import { runModules } from '~/run_modules';
+
+const modules = import.meta.glob('../../../../jh/app/assets/javascripts/pages/**/index.js');
+
+runModules(modules, '../../../../jh/app/assets/javascripts/pages/');
diff --git a/app/assets/javascripts/entrypoints/performance_bar.js b/app/assets/javascripts/entrypoints/performance_bar.js
new file mode 100644
index 00000000000..3f6fc6272d0
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/performance_bar.js
@@ -0,0 +1 @@
+import '../performance_bar';
diff --git a/app/assets/javascripts/entrypoints/redirect_listbox.js b/app/assets/javascripts/entrypoints/redirect_listbox.js
new file mode 100644
index 00000000000..811a73fbf2f
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/redirect_listbox.js
@@ -0,0 +1 @@
+import './behaviors/redirect_listbox';
diff --git a/app/assets/javascripts/entrypoints/sandboxed_mermaid.js b/app/assets/javascripts/entrypoints/sandboxed_mermaid.js
new file mode 100644
index 00000000000..d3dd144ffba
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/sandboxed_mermaid.js
@@ -0,0 +1 @@
+import '../lib/mermaid';
diff --git a/app/assets/javascripts/entrypoints/sentry.js b/app/assets/javascripts/entrypoints/sentry.js
new file mode 100644
index 00000000000..debafc6fab3
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/sentry.js
@@ -0,0 +1 @@
+import '../sentry/index';
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue
index 314de21ffe4..7f25ca8a94d 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue
@@ -22,13 +22,9 @@ export default {
{{ commit.title }} {{ commit.title }}