App initialization development guidelines (repository and blob pages)
The repository (tree) page and the blob page mount the same shared Vue apps, but different entry points bootstrap them depending on how the user reaches the page. The following sections describe which apps mount in each flow and what each entry point must initialize.
In this page, ~/ is an alias for app/assets/javascripts, and the words “tree” and
“repository” are used interchangeably.
Summary
The tree page bundle initializes the apps when a user navigates to a blob from the repository page. The blob page bundle initializes the apps when a user loads a blob URL directly, for example when refreshing the page. The first flow is faster because only the content area is swapped out.
Both flows call the same shared bootstrap,
initRepositoryPage(), so they create the same Vue Router and mount the same
shared repository app (~/repository/components/legacy_app.vue), and tree and blob
navigation is client-side in both directions. The difference between the two flows
is which page bundle calls the bootstrap, and which HAML provides the mount points.
As a result, each entry point must initialize the apps with the correct data for all of these flows:
- Tree page, then client-side navigation to a blob.
- Direct blob page access.
- Project overview page, then client-side navigation to a blob.
Router
In ~/repository/router.js, the route path determines the rendered Vue component:
TreePage:~/repository/pages/tree.vueBlobPage:~/repository/pages/blob.vueIndexPage:~/repository/pages/index.vue
Each tree and blob route is registered multiple times (*PathDecoded,
*PathEncoded, *PathEncodedSlashes, and plain) so refs with special characters
(#, %, and others) resolve regardless of how the backend encoded them.
Shared bootstrap
initRepositoryPage() (~/repository/init_repository_page.js) wires up everything
both pages share. In order:
- Picks the main mount point:
#js-tree-list(tree page) or#js-view-blob-app(blob page), and returnsnullwhen neither exists. createRouter(projectPath, escapedRef, fullName). The router base must use the pre-encoded ref: the route patterns embed encoded variants of it, so a raw ref containing#or%would double-encode.- Apollo cache writes for the
projectPath,projectShortPath, andrefqueries. These happen before any app mounts because the apps read them from the cache. On blob pages, the short path comes from the header mount point’s dataset until the consolidated helper from work item 630950 lands. initFileTreeBrowser(#js-file-browser):~/repository/file_tree_browser/.initHeaderApp(#js-repository-blob-header-app):~/repository/init_header_app.js. The header area app also renders the breadcrumbs and the Web IDE button.initLastCommitApp(#js-last-commit):~/repository/init_last_commit_app.js.initForkInfoApp(#js-fork-info):~/repository/init_fork_info_app.js.- The main repository app (
~/repository/components/legacy_app.vue) on the mount point from the first step.
It returns { router, data, apolloProvider, projectPath } so the EE wrappers can mount
the code owners app against the same router and Apollo provider instead of
creating duplicates.
Single repository app
The repository_single_vue_app feature flag replaces the separate mount points on the
tree and blob pages with one mount point, #js-repository-app, rendered by
app/views/projects/_repository_app.html.haml.
The page data comes from TreeHelper#vue_repository_app_data.
Epic 23817 tracks the work
to fold every repository app into this root.
When #js-repository-app exists, initRepositoryPage() delegates to
initRepositoryApp() (~/repository/init_repository_app.js) and skips the legacy
mount points. initRepositoryApp():
- Parses the dataset once with
parseRepositoryAppDataset()(~/repository/utils/parse_repository_app_dataset.js). JSON values fall back to an empty value instead of throwing, because one malformed value would otherwise stop the whole page from mounting. - Creates the router and seeds the Apollo cache, like the legacy path.
- Mounts
~/repository/components/app.vuewith a singleprovidethat covers the header area and the repository app. - Returns the same
{ router, data, apolloProvider, projectPath }shape as the legacy path.
app.vue renders one wrapper element, which Vue 2 requires. The wrapper
contains the file tree browser, the header area, the fork information, the last commit,
and the <router-view>.
On blob routes, app.vue also renders ~/repository/components/blob_auxiliary_viewer.vue
inside the last commit container, where _blob.html.haml places the server-rendered
auxiliary viewer. The single repository app layout does not render that partial or the
#js-blob-auxiliary-viewer mount point. The component is keyed on the path, so it
loads the notice again after every client-side navigation.
Both page bundles also check for #js-repository-app:
~/pages/projects/show/index.jsloads the repository bundle when#js-tree-listor#js-repository-appexists.initBlobShowcalls the shared bootstrap andinitBlobwhen#js-repository-appexists, instead of falling back toBlobViewer.
File tree browser
Both the tree page load and the direct blob page load mount the file tree browser
with initFileTreeBrowser (~/repository/file_tree_browser/index.js):
- Both HAML entry points render the mount point
#js-file-browserwith a pre-reserved width of 320 pixels to prevent layout shift. The width must matchTREE_WIDTHinfile_tree_browser.vue. - The file tree browser shares the router instance with the repository app, so selecting a file in it is a client-side navigation. Tree to blob and blob to blob navigation happen without a full page load.
- The file tree browser renders on tree and blob routes, but hides itself on the
project root route (
projectRoot). - A Pinia store keeps the expanded, peek, and visible state, so the state persists across route changes.
Tree page load
HAML partials:
app/views/projects/tree/show.html.hamlrendersapp/views/projects/_files.html.haml, which rendersapp/views/projects/_tree_content.html.haml. These provide#js-tree-list,#js-last-commit, and the other mount points.
Initialization:
~/pages/projects/tree/show/index.jsimportsee_else_ce/repositoryand callsinitTree(),initAmbiguousRefModal()(#js-ambiguous-ref-modal), andaddShortcutsExtension(ShortcutsNavigation).- CE:
~/repository/index.js(setupVueRepositoryList) registers theSimpleViewerandBlobContentperformance plugin components and delegates to the shared bootstrap, which mounts everything on this page. - EE:
ee/app/assets/javascripts/repository/index.jswraps the CE init and additionally mounts the code owners app (#js-code-owners) with the router and Apollo provider thatsetupVueRepositoryListreturns.
- CE:
Vue apps on the page:
~/repository/components/header_area.vue~/repository/components/last_commit.vue~/repository/components/legacy_app.vue, which renders~/repository/pages/tree.vueand~/repository/components/tree_content.vue~/repository/file_tree_browser/file_tree_browser.vue~/repository/components/fork_info.vue~/vue_shared/components/ref/components/ambiguous_ref_modal.vueee/app/assets/javascripts/vue_shared/components/code_owners/code_owners.vue(EE only)
Tree to blob navigation (client-side)
No apps are re-initialized when a user navigates from the tree page to a blob.
The router swaps TreePage for BlobPage inside the already-mounted repository app
(app.vue renders <router-view>), and the other apps (header area, last commit,
and file tree browser) react to the route change on their own.
The fast path swaps out only the content area. Everything else stays mounted.
One consequence: a blob reached through client-side navigation never runs
show_blob_bundle.js, so anything set up only by that bundle (for example,
initAuxiliaryViewer and the legacy initBlob behaviors) is not initialized
in this flow. Equivalent
functionality has to either live inside the shared Vue apps (for example,
last_commit.vue fetches the pipeline status itself through GraphQL) or be mounted
by both entry points (for example, code owners).
Direct blob page access
A direct blob page access happens when a user loads or refreshes a blob URL.
HAML partials:
app/views/projects/blob/show.html.hamlrendersapp/views/projects/blob/_blob.html.haml, which provides#js-view-blob-app.
Initialization:
~/pages/projects/blob/show/index.jsis a thin shim. The logic lives in~/blob/show/show_blob_bundle.js(initBlobShow).- The EE page entry calls
initEEBlobShow(ee/app/assets/javascripts/blob/show/blob_show_bundle.js), which callsinitBlobShowand additionally mounts the code owners app (#js-code-owners). initBlobShowalways runsinitPerformancePlugin,initAmbiguousRefModal(#js-ambiguous-ref-modal), andshowAlertFromLocalStorage.- When
#js-view-blob-appexists: the shared bootstrap, which mounts everything including the repository app renderingBlobPage, followed byinitAuxiliaryViewerandinitBlob(legacy blob behaviors: line links, blame shortcuts, and others). - Fallback when
#js-view-blob-appdoes not exist, which happens on the expanded blob view (?expanded=true):new BlobViewer(),initBlob(), andinitForkInfoApp(#js-fork-info). The dynamic~/code_navigationimport also runs only in this fallback, and only when#js-code-navigationexists. - In both cases the bundle calls
initWebIdeLink(#js-blob-web-ide-link), but its mount point comes from the blob header HAML (app/views/projects/blob/_header.html.haml), which_blob.html.hamlrenders only in the expanded view. On the Vue blob page the call returns early because the header area app renders the Web IDE button. - The table of contents for rich viewers is rendered by
blob_header.vueinside the Vue blob page, so it has no separate mount point.
Vue apps on the page:
~/repository/components/header_area.vue~/repository/components/last_commit.vue~/repository/components/legacy_app.vue, which renders~/repository/pages/blob.vueand~/repository/components/blob_content_viewer.vue~/repository/file_tree_browser/file_tree_browser.vue~/repository/components/fork_info.vue~/vue_shared/components/ref/components/ambiguous_ref_modal.vueee/app/assets/javascripts/vue_shared/components/code_owners/code_owners.vue(EE only)
Blob to tree navigation (client-side)
Blob to tree navigation is also client-side: the router swaps BlobPage for
TreePage inside the already-mounted repository app. Because nothing tears the
page down, anything set up only by show_blob_bundle.js on the direct blob load
(initAuxiliaryViewer and the legacy initBlob behaviors) stays in place after
the user leaves the blob route.
Project overview page
The project overview page uses different HAML partials depending on the user’s
project overview setting (Preferences > Behavior > Project overview content) and
on whether the repository is empty. ~/pages/projects/show/index.js conditionally
initializes apps with dynamic imports:
#js-tree-listpresent: dynamic import ofee_else_ce/repository, the same tree initialization as above..blob-viewerpresent (README): dynamic import of~/blob/viewer(new BlobViewer()) andinitHeaderApp({ isReadmeView: true })..project-show-activitypresent: dynamic import of~/activities.
Regardless of the overview content, initWebIdeLink (#js-tree-web-ide-link)
always runs.
Related partials:
app/views/projects/_readme.html.hamlapp/views/projects/_activity.html.haml
For an empty repository, initCodeDropdown
(#js-project-show-empty-page #js-code-dropdown) also lives in
~/pages/projects/show/index.js and mounts
ee_else_ce/repository/components/code_dropdown/compact_code_dropdown.vue.
The mount point comes from app/views/projects/buttons/_code.html.haml, rendered by
app/views/projects/empty.html.haml.