From 11501d600a7abd3a4e547fd1973e3188cbe61222 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 6 Sep 2024 00:13:51 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../javascripts/members/graphql_client.js | 3 + app/assets/javascripts/members/index.js | 6 +- .../application_utilities_to_be_replaced.scss | 4 - ...ication_utilities_to_be_replaced_dark.scss | 3 - app/assets/stylesheets/framework.scss | 1 - app/assets/stylesheets/framework/diffs.scss | 77 +-- app/assets/stylesheets/framework/lists.scss | 2 +- app/assets/stylesheets/framework/mixins.scss | 15 + app/assets/stylesheets/framework/notes.scss | 14 - .../page_bundles/notes/_diff_comments.scss | 376 +++++++++++++ .../page_bundles/notes/_image_comments.scss | 73 +++ .../page_bundles/notes/_note_actions.scss | 75 +++ .../page_bundles/notes_shared.scss | 1 + app/assets/stylesheets/pages/notes.scss | 451 --------------- config/tailwind.config.js | 28 - config/vite.json | 1 - .../cloud/aws/gitlab_aws_integration.md | 2 +- doc/topics/git/merge.md | 6 + .../dast/authentication.md | 1 + doc/user/project/import/github.md | 7 + .../merge_requests/duo_in_merge_requests.md | 4 +- lib/gitlab/git/blame.rb | 2 +- locale/gitlab.pot | 3 + package.json | 6 +- qa/qa/page/component/content_editor.rb | 7 +- scripts/frontend/compare_css_util_classes.mjs | 174 ------ scripts/frontend/lib/tailwind_migration.mjs | 363 ------------ scripts/frontend/tailwind_all_the_way.mjs | 255 --------- scripts/frontend/tailwind_equivalents.json | 530 ------------------ .../tailwind_lint_against_legacy_utils.js | 220 ++++++++ scripts/frontend/tailwindcss.mjs | 30 +- scripts/frontend/vite | 9 - ...tailwind_lint_against_legacy_utils_spec.js | 40 ++ spec/lib/gitlab/git/blame_spec.rb | 22 + yarn.lock | 5 - 35 files changed, 868 insertions(+), 1948 deletions(-) create mode 100644 app/assets/javascripts/members/graphql_client.js delete mode 100644 app/assets/stylesheets/application_utilities_to_be_replaced.scss delete mode 100644 app/assets/stylesheets/application_utilities_to_be_replaced_dark.scss delete mode 100644 app/assets/stylesheets/framework/notes.scss create mode 100644 app/assets/stylesheets/page_bundles/notes/_diff_comments.scss create mode 100644 app/assets/stylesheets/page_bundles/notes/_image_comments.scss create mode 100644 app/assets/stylesheets/page_bundles/notes/_note_actions.scss delete mode 100755 scripts/frontend/compare_css_util_classes.mjs delete mode 100644 scripts/frontend/lib/tailwind_migration.mjs delete mode 100755 scripts/frontend/tailwind_all_the_way.mjs delete mode 100644 scripts/frontend/tailwind_equivalents.json create mode 100755 scripts/frontend/tailwind_lint_against_legacy_utils.js delete mode 100755 scripts/frontend/vite create mode 100644 spec/frontend/scripts/frontend/tailwind_lint_against_legacy_utils_spec.js diff --git a/app/assets/javascripts/members/graphql_client.js b/app/assets/javascripts/members/graphql_client.js new file mode 100644 index 00000000000..f04c023db16 --- /dev/null +++ b/app/assets/javascripts/members/graphql_client.js @@ -0,0 +1,3 @@ +import createDefaultClient from '~/lib/graphql'; + +export const graphqlClient = createDefaultClient(); diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js index 0a64e93aa1c..746fed8cf0d 100644 --- a/app/assets/javascripts/members/index.js +++ b/app/assets/javascripts/members/index.js @@ -3,11 +3,11 @@ import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import VueApollo from 'vue-apollo'; -import createDefaultClient from '~/lib/graphql'; import { parseDataAttributes } from '~/members/utils'; import { TABS } from 'ee_else_ce/members/tabs_metadata'; import MembersTabs from './components/members_tabs.vue'; import membersStore from './store'; +import { graphqlClient } from './graphql_client'; /** * @param {HTMLElement} el @@ -63,7 +63,9 @@ export const initMembersApp = (el, context, options) => { name: 'MembersRoot', components: { MembersTabs }, store, - apolloProvider: new VueApollo({ defaultClient: createDefaultClient() }), + apolloProvider: new VueApollo({ + defaultClient: graphqlClient, + }), provide: { currentUserId: gon.current_user_id || null, sourceId, diff --git a/app/assets/stylesheets/application_utilities_to_be_replaced.scss b/app/assets/stylesheets/application_utilities_to_be_replaced.scss deleted file mode 100644 index f8c40835a49..00000000000 --- a/app/assets/stylesheets/application_utilities_to_be_replaced.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import 'page_bundles/mixins_and_variables_and_functions'; - -// Gitlab UI util classes -@import '@gitlab/ui/src/scss/utilities'; diff --git a/app/assets/stylesheets/application_utilities_to_be_replaced_dark.scss b/app/assets/stylesheets/application_utilities_to_be_replaced_dark.scss deleted file mode 100644 index 3c7b3e63b59..00000000000 --- a/app/assets/stylesheets/application_utilities_to_be_replaced_dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './themes/dark'; - -@import 'application_utilities_to_be_replaced'; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 147c089fa6a..9c78bf693dc 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -37,7 +37,6 @@ @import 'framework/super_sidebar'; @import 'framework/brand_logo'; @import 'framework/tables'; -@import 'framework/notes'; @import 'framework/tabs'; @import 'framework/timeline'; @import 'framework/typography'; diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index d4c4c7c45f5..4222d9c56e3 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -1,3 +1,6 @@ +@import './page_bundles/notes/diff_comments'; +@import './page_bundles/notes/image_comments'; + $diff-file-header: 41px; // Common @@ -777,69 +780,6 @@ table.code { cursor: auto; } -.frame.click-to-comment, -.btn-transparent.image-diff-overlay-add-comment { - position: relative; - cursor: url('illustrations/image_comment_light_cursor.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset, - auto; - - // Retina cursor - cursor: image-set(url('illustrations/image_comment_light_cursor.svg') 1x, - url('illustrations/image_comment_light_cursor@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset, - auto; - - .comment-indicator { - position: absolute; - padding: 0; - width: (2px * $image-comment-cursor-left-offset); - height: (2px * $image-comment-cursor-top-offset); - color: $blue-400; - // center the indicator to match the top left click region - margin-top: (-1px * $image-comment-cursor-top-offset) + 2; - margin-left: (-1px * $image-comment-cursor-left-offset) + 1; - - svg { - width: 100%; - height: 100%; - } - - &:focus { - outline: none; - } - } -} - -.frame .image-comment-badge, -.frame .comment-indicator { - // Center align badges on the frame - transform: translate(-50%, -50%); -} - -.image-comment-badge { - position: absolute; - width: 24px; - height: 24px; - padding: 0; - background: none; - border: 0; - - > svg { - width: 100%; - height: 100%; - } -} - -.image-diff-avatar-link, -.user-avatar-link { - position: relative; - - .badge.badge-pill, - .image-comment-badge { - top: 25px; - right: 8px; - } -} - .discussion-notes { min-height: 35px; background-color: transparent; @@ -896,17 +836,6 @@ table.code { } } -.image-diff-overlay, -.image-diff-overlay-add-comment { - top: 0; - left: 0; - - &:active, - &:focus { - outline: 0; - } -} - .diff-suggest-popover { &.popover { width: 250px; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index c53e376e53e..17f99b5662f 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -93,7 +93,7 @@ ul.content-list { font-weight: $gl-font-weight-bold; } - a { + a:not(.gfm-project_member) { color: $gl-text-color; &.inline-link { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index fd165768e8a..54456f9bf99 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -445,6 +445,21 @@ } } +@mixin notes-media($condition, $breakpoint-width) { + @media (#{$condition}-width: ($breakpoint-width)) { + @content; + } + + // Diff is side by side + .notes-content.parallel & { + // We hide at double what we normally hide at because + // there are two columns of notes + @media (#{$condition}-width: (2 * $breakpoint-width)) { + @content; + } + } +} + /** * Style to apply to, for example, search results matches or text wrapped * around (allowed in Markdown). diff --git a/app/assets/stylesheets/framework/notes.scss b/app/assets/stylesheets/framework/notes.scss deleted file mode 100644 index 85ddf11d6fe..00000000000 --- a/app/assets/stylesheets/framework/notes.scss +++ /dev/null @@ -1,14 +0,0 @@ -@mixin notes-media($condition, $breakpoint-width) { - @media (#{$condition}-width: ($breakpoint-width)) { - @content; - } - - // Diff is side by side - .notes-content.parallel & { - // We hide at double what we normally hide at because - // there are two columns of notes - @media (#{$condition}-width: (2 * $breakpoint-width)) { - @content; - } - } -} diff --git a/app/assets/stylesheets/page_bundles/notes/_diff_comments.scss b/app/assets/stylesheets/page_bundles/notes/_diff_comments.scss new file mode 100644 index 00000000000..1ddb70e50fc --- /dev/null +++ b/app/assets/stylesheets/page_bundles/notes/_diff_comments.scss @@ -0,0 +1,376 @@ +/** +* Line note button on the side of diffs +*/ + +.diff-grid-left:hover, +.diff-grid-right:hover, +.line_holder .is-over:not(.no-comment-btn) { + .add-diff-note { + opacity: 1; + z-index: 101; + } +} + +.tooltip-wrapper.add-diff-note { + margin-left: -52px; + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 10; +} + +.note-button.add-diff-note { + opacity: 0; + will-change: opacity; + border-radius: 50%; + background: $white; + padding: 1px; + font-size: 12px; + @apply gl-text-link; + border: 1px solid $blue-500; + width: 24px; + height: 24px; + + &:hover, + &.inverted { + background: $blue-500; + border-color: $blue-600; + color: $white; + } + + &:active { + outline: 0; + } + + &[disabled] { + background: $white; + border-color: $gray-200; + @apply gl-text-disabled; + cursor: not-allowed; + } +} + +.unified-diff-components-diff-note-button { + &::before { + background-color: $blue-500; + mask-image: url('icons-stacked.svg#comment'); + mask-repeat: no-repeat; + mask-size: cover; + mask-position: center; + content: ''; + width: 12px; + height: 12px; + } + + &:hover:not([disabled]), + &.inverted { + &::before { + background-color: $white; + } + } +} + +.disabled-comment { + @apply gl-text-disabled gl-bg-subtle gl-border-b gl-rounded-base; + padding: $gl-padding-8 0; + + a:not(.learn-more) { + @apply gl-text-link; + } +} + +// Vue refactored diff discussion adjustments +.files { + .diff-discussions { + .note-discussion.timeline-entry { + padding-left: 0; + + ul.notes li.note-wrapper { + .timeline-content { + padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding; + } + + .timeline-avatar { + margin: $gl-padding-8 0 0 $gl-padding; + } + } + + ul.notes { + li.toggle-replies-widget { + margin-left: 0; + border-left: 0; + border-right: 0; + border-radius: 0 !important; + } + + .discussion-reply-holder { + margin-left: 0; + } + } + + &:last-child { + border-bottom: 0; + } + + > .timeline-entry-inner { + padding: 0; + + > .timeline-content { + margin-left: 0; + } + + > .timeline-icon { + display: none; + } + } + + .discussion-body { + padding-top: 0; + + .discussion-wrapper { + border: 0; + } + } + } + } + + .diff-comment-form { + display: block; + } +} + +.discussion-filter-container { + .dropdown-menu { + margin-bottom: $gl-padding-4; + } +} + +// Diff code in discussion view +.discussion-body .diff-file { + .file-title { + cursor: default; + border-top: 0; + border-radius: 0; + margin-left: $note-spacing-left; + + &:hover { + @apply gl-bg-subtle; + } + } + + .line_content { + white-space: pre-wrap; + } + + .diff-content { + margin-left: $note-spacing-left; + + .line_holder td:first-of-type { + @include gl-border-l; + } + + .line_holder td:last-of-type { + @include gl-border-r; + } + + .discussion-notes { + margin-left: -$note-spacing-left; + + .notes { + background-color: transparent; + } + + .notes-content { + border: 0; + } + + .timeline-content { + border-top: 0 !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + } + } + } +} + +.tab-pane.notes { + .diff-file .notes .system-note { + margin: 0; + } +} + +.tab-pane.diffs { + .system-note { + padding: 0 $gl-padding; + margin-left: 20px; + } + + .notes > .note-discussion li.note.system-note { + border-bottom: 0; + padding: 0; + } +} + +.diff-file { + .diff-grid-left:hover, + .diff-grid-right:hover, + .is-over { + .add-diff-note { + display: inline-flex; + justify-content: center; + align-items: center; + } + } + + .discussion-notes { + &:not(:last-child) { + margin-bottom: 0; + } + + .system-note { + background-color: transparent; + padding: 0; + } + } + + // Merge request notes in diffs + // Diff is inline + .notes-content .note-header .note-headline-light { + display: inline-block; + position: relative; + } + + .notes_holder { + font-family: $regular-font; + + .diff-td, + td { + @apply gl-border; + border-left: 0; + + .discussion-notes .timeline-entry:first-of-type > .timeline-entry-inner { + @apply gl-bg-default; + + // stylelint-disable-next-line gitlab/no-gl-class + .gl-dark & { + @apply gl-bg-strong gl-border-b-default; + } + + .toggle-replies-widget { + @apply gl-border-b-subtle; + } + + .toggle-replies-widget[aria-expanded="false"] { + @apply gl-border-b-0; + } + } + + .notes > .discussion-reply-holder { + &:first-child { + padding-top: $gl-padding-12; + } + + &:not(:first-child):not(:nth-child(2)) { + padding-top: 0; + } + } + + &.notes-content { + border-width: 1px 0; + padding: 0; + vertical-align: top; + white-space: normal; + @apply gl-bg-subtle; + + &.parallel { + border-width: 1px; + + &.new { + border-right-width: 0; + } + + .note-header { + flex-wrap: wrap; + align-items: center; + } + } + + .notes { + @apply gl-bg-subtle; + } + + a code { + top: 0; + margin-right: 0; + } + } + } + } + + .diff-grid-comments:last-child { + .notes-content { + border-bottom-width: 0; + border-bottom-left-radius: $gl-border-radius-base-inner; + border-bottom-right-radius: $gl-border-radius-base-inner; + } + } +} + +.diff-files-holder { + .discussion-notes .timeline-entry:first-of-type > .timeline-entry-inner { + @apply gl-border-b gl-border-b-subtle; + } +} + +.diffs { + .discussion-notes { + margin-left: 0; + border-left: 0; + } + + .note-wrapper { + &.system-note { + border: 0; + margin-left: 20px; + } + } + + .discussion-reply-holder { + border-top: 0; + @apply gl-rounded-t-base; + position: relative; + + .discussion-form { + width: 100%; + @apply gl-bg-subtle; + padding: 0; + } + + .disabled-comment { + padding: $gl-vert-padding 0; + width: 100%; + } + } +} + +.code-commit .notes-content, +.diff-viewer > .image ~ .note-container { + @apply gl-bg-default; + + li.note-comment { + padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding; + + .avatar { + margin-right: 0; + } + + .note-body { + padding: $gl-padding-4 0 $gl-padding-8; + margin-left: $note-spacing-left; + } + } +} + +.diff-viewer > .image ~ .note-container form.new-note { + margin-left: 0; +} diff --git a/app/assets/stylesheets/page_bundles/notes/_image_comments.scss b/app/assets/stylesheets/page_bundles/notes/_image_comments.scss new file mode 100644 index 00000000000..d6aa4d2b500 --- /dev/null +++ b/app/assets/stylesheets/page_bundles/notes/_image_comments.scss @@ -0,0 +1,73 @@ +.frame.click-to-comment, +.btn-transparent.image-diff-overlay-add-comment { + position: relative; + cursor: url('illustrations/image_comment_light_cursor.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset, + auto; + + // Retina cursor + cursor: image-set(url('illustrations/image_comment_light_cursor.svg') 1x, + url('illustrations/image_comment_light_cursor@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset, + auto; + + .comment-indicator { + position: absolute; + padding: 0; + width: (2px * $image-comment-cursor-left-offset); + height: (2px * $image-comment-cursor-top-offset); + color: $blue-400; + // center the indicator to match the top left click region + margin-top: (-1px * $image-comment-cursor-top-offset) + 2; + margin-left: (-1px * $image-comment-cursor-left-offset) + 1; + + svg { + width: 100%; + height: 100%; + } + + &:focus { + outline: none; + } + } +} + +.frame .image-comment-badge, +.frame .comment-indicator { + // Center align badges on the frame + transform: translate(-50%, -50%); +} + +.image-comment-badge { + position: absolute; + width: 24px; + height: 24px; + padding: 0; + background: none; + border: 0; + + > svg { + width: 100%; + height: 100%; + } +} + +.image-diff-avatar-link, +.user-avatar-link { + position: relative; + + .badge.badge-pill, + .image-comment-badge { + top: 25px; + right: 8px; + } +} + +.image-diff-overlay, +.image-diff-overlay-add-comment { + top: 0; + left: 0; + + &:active, + &:focus { + outline: 0; + } +} diff --git a/app/assets/stylesheets/page_bundles/notes/_note_actions.scss b/app/assets/stylesheets/page_bundles/notes/_note_actions.scss new file mode 100644 index 00000000000..1ec152f1264 --- /dev/null +++ b/app/assets/stylesheets/page_bundles/notes/_note_actions.scss @@ -0,0 +1,75 @@ +/** + * Actions for Discussions/Notes + */ + + .discussion-actions { + float: right; + + @include media-breakpoint-down(xs) { + width: 100%; + margin: 0 0 $gl-padding-8; + } + + .btn-group > .discussion-next-btn { + margin-left: -1px; + } + + .btn-group > .discussion-create-issue-btn { + margin-left: -2px; + } + + svg { + height: 15px; + } +} + +.note-actions { + justify-content: flex-end; + flex-shrink: 1; + display: inline-flex; + align-items: center; + margin-left: $gl-padding-8; + @apply gl-text-subtle; + + @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { + justify-content: flex-start; + float: none; + + .note-actions__mobile-spacer { + flex-grow: 1; + } + } +} + +.more-actions { + display: flex; + align-items: flex-end; + + .tooltip { + white-space: nowrap; + } +} + +.more-actions-dropdown { + width: 180px; + min-width: 180px; +} + +.discussion-toggle-button { + padding: 0 $gl-padding-8 0 0; + background-color: transparent; + border: 0; + line-height: 20px; + font-size: 13px; + transition: color 0.1s linear; + + &:hover, + &:focus { + @apply gl-text-link; + } + + &:focus { + text-decoration: underline; + outline: none; + } +} diff --git a/app/assets/stylesheets/page_bundles/notes_shared.scss b/app/assets/stylesheets/page_bundles/notes_shared.scss index d4ddab8739e..8c799692ea9 100644 --- a/app/assets/stylesheets/page_bundles/notes_shared.scss +++ b/app/assets/stylesheets/page_bundles/notes_shared.scss @@ -1,2 +1,3 @@ @import 'mixins_and_variables_and_functions'; @import './notes/system_notes_v2'; +@import './notes/note_actions'; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index ffadcd9671e..3c1b4456d39 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -455,237 +455,6 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio; } } - -// Diff code in discussion view -.discussion-body .diff-file { - .file-title { - cursor: default; - border-top: 0; - border-radius: 0; - margin-left: $note-spacing-left; - - &:hover { - @apply gl-bg-subtle; - } - } - - .line_content { - white-space: pre-wrap; - } - - .diff-content { - margin-left: $note-spacing-left; - - .line_holder td:first-of-type { - @include gl-border-l; - } - - .line_holder td:last-of-type { - @include gl-border-r; - } - - .discussion-notes { - margin-left: -$note-spacing-left; - - .notes { - background-color: transparent; - } - - .notes-content { - border: 0; - } - - .timeline-content { - border-top: 0 !important; - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; - } - } - } -} - -.tab-pane.notes { - .diff-file .notes .system-note { - margin: 0; - } -} - -.tab-pane.diffs { - .system-note { - padding: 0 $gl-padding; - margin-left: 20px; - } - - .notes > .note-discussion li.note.system-note { - border-bottom: 0; - padding: 0; - } -} - -.diff-file { - .diff-grid-left:hover, - .diff-grid-right:hover, - .is-over { - .add-diff-note { - display: inline-flex; - justify-content: center; - align-items: center; - } - } - - .discussion-notes { - &:not(:last-child) { - margin-bottom: 0; - } - - .system-note { - background-color: transparent; - padding: 0; - } - } - - // Merge request notes in diffs - // Diff is inline - .notes-content .note-header .note-headline-light { - display: inline-block; - position: relative; - } - - .notes_holder { - font-family: $regular-font; - - .diff-td, - td { - @apply gl-border; - border-left: 0; - - .discussion-notes .timeline-entry:first-of-type > .timeline-entry-inner { - @apply gl-bg-default; - - // stylelint-disable-next-line gitlab/no-gl-class - .gl-dark & { - @apply gl-bg-strong gl-border-b-default; - } - - .toggle-replies-widget { - @apply gl-border-b-subtle; - } - - .toggle-replies-widget[aria-expanded="false"] { - @apply gl-border-b-0; - } - } - - .notes > .discussion-reply-holder { - &:first-child { - padding-top: $gl-padding-12; - } - - &:not(:first-child):not(:nth-child(2)) { - padding-top: 0; - } - } - - &.notes-content { - border-width: 1px 0; - padding: 0; - vertical-align: top; - white-space: normal; - @apply gl-bg-subtle; - - &.parallel { - border-width: 1px; - - &.new { - border-right-width: 0; - } - - .note-header { - flex-wrap: wrap; - align-items: center; - } - } - - .notes { - @apply gl-bg-subtle; - } - - a code { - top: 0; - margin-right: 0; - } - } - } - } - - .diff-grid-comments:last-child { - .notes-content { - border-bottom-width: 0; - border-bottom-left-radius: $gl-border-radius-base-inner; - border-bottom-right-radius: $gl-border-radius-base-inner; - } - } -} - -.diff-files-holder { - .discussion-notes .timeline-entry:first-of-type > .timeline-entry-inner { - @apply gl-border-b gl-border-b-subtle; - } -} - -.diffs { - .discussion-notes { - margin-left: 0; - border-left: 0; - } - - .note-wrapper { - &.system-note { - border: 0; - margin-left: 20px; - } - } - - .discussion-reply-holder { - border-top: 0; - @apply gl-rounded-t-base; - position: relative; - - .discussion-form { - width: 100%; - @apply gl-bg-subtle; - padding: 0; - } - - .disabled-comment { - padding: $gl-vert-padding 0; - width: 100%; - } - } -} - -.code-commit .notes-content, -.diff-viewer > .image ~ .note-container { - @apply gl-bg-default; - - li.note-comment { - padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding; - - .avatar { - margin-right: 0; - } - - .note-body { - padding: $gl-padding-4 0 $gl-padding-8; - margin-left: $note-spacing-left; - } - } -} - -.diff-viewer > .image ~ .note-container form.new-note { - margin-left: 0; -} - .discussion-header, .note-header-info { a { @@ -829,228 +598,8 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio; } } -/** - * Actions for Discussions/Notes - */ -.discussion-actions { - float: right; - @include media-breakpoint-down(xs) { - width: 100%; - margin: 0 0 $gl-padding-8; - } - - .btn-group > .discussion-next-btn { - margin-left: -1px; - } - - .btn-group > .discussion-create-issue-btn { - margin-left: -2px; - } - - svg { - height: 15px; - } -} - -.note-actions { - justify-content: flex-end; - flex-shrink: 1; - display: inline-flex; - align-items: center; - margin-left: $gl-padding-8; - @apply gl-text-subtle; - - @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { - justify-content: flex-start; - float: none; - - .note-actions__mobile-spacer { - flex-grow: 1; - } - } -} - -.more-actions { - display: flex; - align-items: flex-end; - - .tooltip { - white-space: nowrap; - } -} - -.more-actions-dropdown { - width: 180px; - min-width: 180px; -} - -.discussion-toggle-button { - padding: 0 $gl-padding-8 0 0; - background-color: transparent; - border: 0; - line-height: 20px; - font-size: 13px; - transition: color 0.1s linear; - - &:hover, - &:focus { - @apply gl-text-link; - } - - &:focus { - text-decoration: underline; - outline: none; - } -} - -/** - * Line note button on the side of diffs - */ - -.diff-grid-left:hover, -.diff-grid-right:hover, -.line_holder .is-over:not(.no-comment-btn) { - .add-diff-note { - opacity: 1; - z-index: 101; - } -} - -.tooltip-wrapper.add-diff-note { - margin-left: -52px; - position: absolute; - top: 50%; - transform: translateY(-50%); - z-index: 10; -} - -.note-button.add-diff-note { - opacity: 0; - will-change: opacity; - border-radius: 50%; - background: $white; - padding: 1px; - font-size: 12px; - @apply gl-text-link; - border: 1px solid $blue-500; - width: 24px; - height: 24px; - - &:hover, - &.inverted { - background: $blue-500; - border-color: $blue-600; - color: $white; - } - - &:active { - outline: 0; - } - - &[disabled] { - background: $white; - border-color: $gray-200; - @apply gl-text-disabled; - cursor: not-allowed; - } -} - -.unified-diff-components-diff-note-button { - &::before { - background-color: $blue-500; - mask-image: url('icons-stacked.svg#comment'); - mask-repeat: no-repeat; - mask-size: cover; - mask-position: center; - content: ''; - width: 12px; - height: 12px; - } - - &:hover:not([disabled]), - &.inverted { - &::before { - background-color: $white; - } - } -} - -.disabled-comment { - @apply gl-text-disabled gl-bg-subtle gl-border-b gl-rounded-base; - padding: $gl-padding-8 0; - - a:not(.learn-more) { - @apply gl-text-link; - } -} - -// Vue refactored diff discussion adjustments -.files { - .diff-discussions { - .note-discussion.timeline-entry { - padding-left: 0; - - ul.notes li.note-wrapper { - .timeline-content { - padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding; - } - - .timeline-avatar { - margin: $gl-padding-8 0 0 $gl-padding; - } - } - - ul.notes { - li.toggle-replies-widget { - margin-left: 0; - border-left: 0; - border-right: 0; - border-radius: 0 !important; - } - - .discussion-reply-holder { - margin-left: 0; - } - } - - &:last-child { - border-bottom: 0; - } - - > .timeline-entry-inner { - padding: 0; - - > .timeline-content { - margin-left: 0; - } - - > .timeline-icon { - display: none; - } - } - - .discussion-body { - padding-top: 0; - - .discussion-wrapper { - border: 0; - } - } - } - } - - .diff-comment-form { - display: block; - } -} - -.discussion-filter-container { - .dropdown-menu { - margin-bottom: $gl-padding-4; - } -} .user-activity-content { position: relative; diff --git a/config/tailwind.config.js b/config/tailwind.config.js index f504083c29d..90fcecae7bc 100644 --- a/config/tailwind.config.js +++ b/config/tailwind.config.js @@ -1,28 +1,5 @@ -const path = require('path'); -const plugin = require('tailwindcss/plugin'); const tailwindDefaults = require('@gitlab/ui/tailwind.defaults'); -// Try loading the tailwind css_in_js, in case they exist -let utilities = {}; -try { - // eslint-disable-next-line global-require, import/extensions - utilities = require('./helpers/tailwind/css_in_js.js'); -} catch (e) { - console.log( - 'config/helpers/tailwind/css_in_js do not exist yet. Please run `scripts/frontend/tailwind_all_the_way.mjs`', - ); - /* - We need to remove the module itself from the cache, because node caches resolved modules. - So if we: - 1. Require this file while helpers/tailwind/css_in_js.js does NOT exist - 2. Require this file again, when it exists, we would get the version from (1.) leading - to errors. - If we bust the cache in case css_in_js.js doesn't exist, we will get the proper version - on a reload. - */ - delete require.cache[path.resolve(__filename)]; -} - /** @type {import('tailwindcss').Config} */ module.exports = { presets: [tailwindDefaults], @@ -40,9 +17,4 @@ module.exports = { // this from happening. For now, we are simply blocking the only problematic occurrence. '[link:page-slug]', ], - plugins: [ - plugin(({ addUtilities }) => { - addUtilities(utilities); - }), - ], }; diff --git a/config/vite.json b/config/vite.json index 01259674368..0d9584e734b 100644 --- a/config/vite.json +++ b/config/vite.json @@ -16,7 +16,6 @@ ], "port": 3038, "publicOutputDir": "vite-dev", - "viteBinPath": "scripts/frontend/vite", "devServerConnectTimeout": 3 } } diff --git a/doc/solutions/cloud/aws/gitlab_aws_integration.md b/doc/solutions/cloud/aws/gitlab_aws_integration.md index a8d54f80ad5..09dcb371f46 100644 --- a/doc/solutions/cloud/aws/gitlab_aws_integration.md +++ b/doc/solutions/cloud/aws/gitlab_aws_integration.md @@ -114,7 +114,7 @@ Generally solutions demonstrate end-to-end capabilities for the development fram - [Enterprise DevOps Blueprint: Serverless Framework Apps on AWS](https://gitlab.com/guided-explorations/aws/serverless/serverless-framework-aws) - working example code and tutorials. `[GitLab Solution]` `[CI Solution]` - [Tutorial: Serverless Framework Deployment to AWS with GitLab Serverless SAST Scanning](https://gitlab.com/guided-explorations/aws/serverless/serverless-framework-aws/-/blob/master/TUTORIAL.md) `[GitLab Solution]` `[CI Solution]` - - [Tutorial: Secure Serverless Framework Development with GitLab Security Policy Approval Rules and Managed DevOps Environments](https://gitlab.com/guided-explorations/aws/serverless/serverless-framework-aws/-/blob/master/TUTORIAL2-SecurityAndManagedEnvs.md) `[GitLab Solution]` `[CI Solution]` + - [Tutorial: Secure Serverless Framework Development with GitLab Security Policy Approval Rules and Managed DevOps Environments](https://gitlab.com/guided-explorations/aws/serverless/serverless-framework-aws/-/blob/prod/TUTORIAL2-SecurityAndManagedEnvs.md?ref_type=heads) `[GitLab Solution]` `[CI Solution]` ### Terraform diff --git a/doc/topics/git/merge.md b/doc/topics/git/merge.md index c3b82b97dbf..503d9afb602 100644 --- a/doc/topics/git/merge.md +++ b/doc/topics/git/merge.md @@ -25,3 +25,9 @@ To get your branch merged into the main branch: 1. If necessary, have your [merge request reviewed](../../user/project/merge_requests/reviews/index.md#request-a-review). 1. Have someone [merge your merge request](../../user/project/merge_requests/index.md#merge-a-merge-request), or merge the merge request yourself, depending on your process. + +## Related topics + +- [Merge requests](../../user/project/merge_requests/index.md) +- [Merge methods](../../user/project/merge_requests/methods/index.md) +- [Merge conflicts](../../user/project/merge_requests/conflicts.md) diff --git a/doc/user/application_security/dast/authentication.md b/doc/user/application_security/dast/authentication.md index 627a7237a9b..bc27bdfaca7 100644 --- a/doc/user/application_security/dast/authentication.md +++ b/doc/user/application_security/dast/authentication.md @@ -404,6 +404,7 @@ dast: - DAST cannot handle multi-factor authentication like one-time passwords (OTP) by using SMS, biometrics, or authenticator apps. Turn these off in the testing environment for the application being scanned. - DAST cannot authenticate to applications that do not set an [authentication token](#authentication-tokens) during login. - DAST cannot authenticate to applications that require more than two inputs to be filled out. Two inputs must be supplied, username and password. +- DAST does not carry the content of IndexedDB into the crawl stage. If your application relies on IndexedDB to maintain authenticated state, [DAST will not be able to authenticate](https://gitlab.com/gitlab-org/gitlab/-/issues/481651) to crawl your application. ## Troubleshooting diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index 3231bc80494..b24e72c4e98 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -95,6 +95,13 @@ If the above requirements are not met, the importer can't map the particular use - GitLab [can't import](https://gitlab.com/gitlab-org/gitlab/-/issues/424046) GitHub Markdown image attachments that were uploaded to private repositories before 2023-05-09. If you encounter this problem, would like to help us resolve the problem, and are willing to provide a sample repository for us, please add a comment to [issue 424046](https://gitlab.com/gitlab-org/gitlab/-/issues/424046) and we'll contact you. +- For [GitLab-specific references](../../markdown.md#gitlab-specific-references), GitLab uses a `#` character for issues and a `!` character for merge requests. + However, GitHub uses only a `#` character for both issues and pull requests. + + When importing: + + - Comment notes, GitLab only matches references to issues because GitLab doesn't know whether a references points to an issue or a merge request. + - Issues or merge request descriptions, GitLab ignores all references because their imported counterparts might not have been created on the destination yet. ## Import your GitHub repository into GitLab diff --git a/doc/user/project/merge_requests/duo_in_merge_requests.md b/doc/user/project/merge_requests/duo_in_merge_requests.md index 2a2b62f5dd7..24e1c3bbfaf 100644 --- a/doc/user/project/merge_requests/duo_in_merge_requests.md +++ b/doc/user/project/merge_requests/duo_in_merge_requests.md @@ -65,7 +65,7 @@ Provide feedback on this experimental feature in [issue 408991](https://gitlab.c ## Generate a merge commit message DETAILS: -**Tier:** For a limited time, Ultimate. In the future, Ultimate with [GitLab Duo Enterprise](../../../subscriptions/subscription-add-ons.md). +**Tier: GitLab.com and Self-managed:** For a limited time, Ultimate. In the future, Ultimate with [GitLab Duo Enterprise](../../../subscriptions/subscription-add-ons.md). **GitLab Dedicated:** GitLab Duo Enterprise. **Offering:** GitLab.com, Self-managed, GitLab Dedicated > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10453) in GitLab 16.2 as an [experiment](../../../policy/experiment-beta-support.md#experiment) [with a flag](../../../administration/feature_flags.md) named `generate_commit_message_flag`. Disabled by default. @@ -85,8 +85,6 @@ To generate a commit message with GitLab Duo: 1. Select **Generate commit message**. 1. Review the commit message provide and choose **Insert** to add it to the commit. -Provide feedback on this experimental feature in [issue 408994](https://gitlab.com/gitlab-org/gitlab/-/issues/408994). - **Data usage**: When you use this feature, the following data is sent to the large language model referenced above: - Contents of the file diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index e134fb31879..2d63bc27f75 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -53,7 +53,7 @@ module Gitlab output.split("\n").each do |line| if line[0, 1] == "\t" lines << line[1, line.size] - elsif m = /^(\w{40}) (\d+) (\d+)\s?(\d+)?/.match(line) + elsif m = /^(\w{40}\w{24}?) (\d+) (\d+)\s?(\d+)?/.match(line) # Removed these instantiations for performance but keeping them for reference: # commit_id, old_lineno, lineno, span = m[1], m[2].to_i, m[3].to_i, m[4].to_i commit_id = m[1] diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3cabc8a0391..502ad53d39f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -43565,6 +43565,9 @@ msgstr "" msgid "PromotionRequests|An error occurred while processing the request" msgstr "" +msgid "PromotionRequests|Error fetching promotion requests count" +msgstr "" + msgid "PromotionRequests|Highest role requested" msgstr "" diff --git a/package.json b/package.json index acfb99bd8c7..144f73e5d83 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "preinternal:stylelint": "yarn run tailwindcss:build", "prejest": "yarn check-dependencies", "build:css": "node scripts/frontend/build_css.mjs", - "tailwindcss:build": "node scripts/frontend/tailwind_all_the_way.mjs --only-used", + "tailwindcss:build": "node scripts/frontend/tailwindcss.mjs", "jest": "jest --config jest.config.js", "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", "jest:ci:build-cache": "./scripts/frontend/warm_jest_cache.mjs", @@ -47,8 +47,7 @@ "lint:stylelint:fix": "yarn run lint:stylelint --fix", "lint:stylelint:staged": "scripts/frontend/execute-on-staged-files.sh stylelint '(css|scss)' -q", "lint:stylelint:staged:fix": "yarn run lint:stylelint:staged --fix", - "lint:tailwind-utils": "REDIRECT_TO_STDOUT=true node scripts/frontend/compare_css_util_classes.mjs", - "prelint:tailwind-utils": "rm -f config/helpers/tailwind/css_in_js.js", + "lint:tailwind-utils": "REDIRECT_TO_STDOUT=true node scripts/frontend/tailwind_lint_against_legacy_utils.js", "markdownlint": "markdownlint-cli2", "preinstall": "node ./scripts/frontend/preinstall.mjs", "postinstall": "node ./scripts/frontend/postinstall.js", @@ -208,7 +207,6 @@ "remark-gfm": "^3.0.1", "remark-parse": "^10.0.2", "remark-rehype": "^10.1.0", - "rgb-hex": "^4.1.0", "sass": "^1.69.7", "scrollparent": "^2.0.1", "semver": "^7.3.4", diff --git a/qa/qa/page/component/content_editor.rb b/qa/qa/page/component/content_editor.rb index d5f93bbc8c9..9d0535eb643 100644 --- a/qa/qa/page/component/content_editor.rb +++ b/qa/qa/page/component/content_editor.rb @@ -31,7 +31,12 @@ module QA text_area.set(text) # wait for text style option to become active after typing has_active_element?('text-styles', wait: 1) - click_element('text-styles') + + retry_until(sleep_interval: 1, message: "Text style dropdown item containing #{heading} did not show up") do + click_element('text-styles') + has_element?('.gl-new-dropdown-contents li', text: heading) + end + find_element('.gl-new-dropdown-contents li', text: heading).click end end diff --git a/scripts/frontend/compare_css_util_classes.mjs b/scripts/frontend/compare_css_util_classes.mjs deleted file mode 100755 index 62233649e3b..00000000000 --- a/scripts/frontend/compare_css_util_classes.mjs +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env node - -/* eslint-disable import/extensions */ - -import { deepEqual } from 'node:assert'; -import { readFile } from 'node:fs/promises'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { - extractRules, - loadCSSFromFile, - normalizeCssInJSDefinition, - darkModeTokenToHex, - mismatchAllowList, -} from './lib/tailwind_migration.mjs'; -import { convertUtilsToCSSInJS, toMinimalUtilities } from './tailwind_all_the_way.mjs'; - -const EQUIV_FILE = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - 'tailwind_equivalents.json', -); - -function darkModeResolver(str) { - return str.replace( - /var\(--([^,]+?), #([a-f\d]{8}|[a-f\d]{6}|[a-f\d]{4}|[a-f\d]{3})\)/g, - (_all, tokenName) => { - if (darkModeTokenToHex[tokenName]) { - return darkModeTokenToHex[tokenName]; - } - - return _all; - }, - ); -} - -function compareApplicationUtilsToTailwind(appUtils, tailwindCSS, colorResolver) { - let fail = 0; - - const tailwind = extractRules(tailwindCSS); - - Object.keys(appUtils).forEach((selector) => { - if (mismatchAllowList.includes(selector)) { - return; - } - - try { - deepEqual( - normalizeCssInJSDefinition(appUtils[selector], colorResolver), - normalizeCssInJSDefinition(tailwind[selector], colorResolver), - ); - } catch (e) { - fail += 1; - console.warn(`Not equal ${selector}`); - console.warn(`Compared: [legacy util => tailwind util]`); - console.warn(e.message.replace(/\n/g, '\n\t')); - } - }); - - if (fail) { - console.log(`\t${fail} selectors failed`); - } else { - console.log('\tAll good'); - } - - return fail; -} - -function ensureNoLegacyUtilIsUsedWithATailwindModifier(minimalUtils) { - let fail = 0; - for (const [key, value] of Object.entries(minimalUtils)) { - if (key.startsWith('.\\!')) { - console.warn('Using legacy util with important modifier. This is not supported.'); - console.warn(`Please migrate ${key} to a proper tailwind util.`); - fail += 1; - } - if (key.endsWith('\\')) { - console.warn(`Using legacy util with ${key} modifier. This is not supported.`); - console.warn(`Please migrate the following classes to a proper tailwind util:`); - console.warn(JSON.stringify(value, null, 2).replace(/^/gm, ' '.repeat(4))); - fail += 1; - } - } - if (fail) { - console.log(`\t${fail} legacy utils with modifiers found`); - } - return fail; -} - -function ensureWeHaveTailwindEquivalentsForLegacyUtils(minimalUtils, equivalents) { - let fail = 0; - - for (const key of Object.keys(minimalUtils)) { - const legacyClassName = key.replace(/^\./, 'gl-').replace('\\', ''); - /* Note: Right now we check that the equivalents are defined, future iteration could be: - !equivalents[legacyClassName] to ensure that all used legacy utils actually have a tailwind equivalent - and not null */ - if (!(legacyClassName in equivalents)) { - console.warn( - `New legacy util (${legacyClassName}) introduced which is untracked in tailwind_equivalents.json.`, - ); - fail += 1; - } - } - if (fail) { - console.log(`\t${fail} unmapped legacy utils found`); - } - return fail; -} - -console.log('# Converting legacy styles to CSS-in-JS definitions'); - -const stats = await convertUtilsToCSSInJS(); - -if (stats.hardcodedColors || stats.potentialMismatches) { - console.warn(`Some utils are not properly mapped`); - process.exitCode = 1; -} - -let failures = 0; - -console.log('# Comparing tailwind to legacy utils'); - -const applicationUtilsLight = extractRules( - loadCSSFromFile('app/assets/builds/application_utilities_to_be_replaced.css'), - { convertColors: true }, -); -const applicationUtilsDark = extractRules( - loadCSSFromFile('app/assets/builds/application_utilities_to_be_replaced_dark.css'), - { convertColors: true }, -); -const tailwind = loadCSSFromFile('app/assets/builds/tailwind.css'); - -console.log('## Comparing tailwind light mode'); -failures += compareApplicationUtilsToTailwind(applicationUtilsLight, tailwind); -console.log('## Comparing tailwind dark mode'); -failures += compareApplicationUtilsToTailwind(applicationUtilsDark, tailwind, darkModeResolver); - -console.log('# Checking whether legacy GitLab utility classes are used'); - -console.log('## Reducing utility definitions to minimally used'); -const { rules } = await toMinimalUtilities(); - -console.log('## Running checks'); -failures += ensureNoLegacyUtilIsUsedWithATailwindModifier(rules); - -console.log('# Checking if we have tailwind equivalents of all classes'); -const equivalents = JSON.parse(await readFile(EQUIV_FILE, 'utf-8')); -failures += ensureWeHaveTailwindEquivalentsForLegacyUtils(rules, equivalents); - -const keys = new Set(Object.keys(rules)); - -// animate-skeleton-loader is alright and will be introduced -// https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/4545 -keys.delete('.animate-skeleton-loader'); - -if (keys.size > 0) { - console.warn('You are introducing legacy utilities:'); - console.warn( - `\t${Array.from(keys) - .map((x) => x.replace(/^\./, '.gl-')) - .sort() - .join('\n\t')}`, - ); - console.warn('Please migrate them to tailwind utilities:'); - console.warn('https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/doc/tailwind-migration.md'); - failures += 1; -} - -if (failures) { - process.exitCode = 1; -} else { - console.log('# All good – Happiness. May the tailwind boost your journey'); -} diff --git a/scripts/frontend/lib/tailwind_migration.mjs b/scripts/frontend/lib/tailwind_migration.mjs deleted file mode 100644 index 23b909526de..00000000000 --- a/scripts/frontend/lib/tailwind_migration.mjs +++ /dev/null @@ -1,363 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import rgbHex from 'rgb-hex'; -import postcss from 'postcss'; -import _ from 'lodash'; - -const ROOT_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../'); -const GITLAB_UI_DIR = path.join(ROOT_PATH, 'node_modules/@gitlab/ui'); - -// This is a list of classes where the tailwind and gitlab-ui output have a mismatch -// This might be due to e.g. the usage of custom properties on tailwinds side, -// or the usage of background vs background-image -export const mismatchAllowList = [ - // Shadows use some `--tw` attributes, but the output should be the same - '.shadow-none', - '.shadow', - '.shadow-sm', - '.shadow-md', - '.shadow-lg', - '.shadow-x0-y2-b4-s0', - '.shadow-x0-y0-b3-s1-blue-500', - // Difference between tailwind and gitlab ui: border-width: 0 vs border: 0 - '.sr-only', - // tailwind uses --tw-rotate and --tw-translate custom properties - // the reason for this: To make translate / rotate composable - // Our utilities would overwrite each other - '.translate-x-0', - '.translate-y-0', - '.rotate-90', - '.rotate-180', - // the border-style utils in tailwind do not allow for top, bottom, right, left - '.border-b-solid', - '.border-l-solid', - '.border-r-solid', - '.border-t-solid', - // Our border shorthand classes are slightly different, - // we migrated them by prepending them to the tailwind.css - '.border', - '.border-b', - '.border-l', - '.border-r', - '.border-t', - '.border\\!', - '.border-b\\!', - '.border-l\\!', - '.border-r\\!', - '.border-t\\!', - // Tailwindy transparent border utils now leverage design tokens, the mismatches are expected. - '.border-transparent', - '.border-t-transparent', - '.border-r-transparent', - '.border-b-transparent', - '.border-l-transparent', - // Tailwind's line-clamp utils don't set `white-space: normal`, while our custom utils did. - // We have added `gl-whitespace-normal` wherever line-clamp utils were being used, so these - // mismatches can be ignored. - '.line-clamp-1', - '.line-clamp-2', - '.line-clamp-3', - '.outline-none', - '.outline-0', - // Tailwind's `bg-none` util applies `background-image: none` while ours does `background: none`. - // Our recommendation is to use `bg-transparent` instead. Existing usages of `bg-none` have been - // migrated to `bg-transparent` as of this comment. - '.bg-none', -]; - -export function loadCSSFromFile(filePath) { - return fs.readFileSync(path.join(ROOT_PATH, filePath), 'utf-8'); -} - -/** - * A map of hex color codes to CSS variables replacements for utils where we can't - * confidently automated the substitutions. - * The keys correspond to a given util's base name obtained with the `selectorToBaseUtilName` helper. - * Values are a map of hex color codes to CSS variable names. - * If no replacement is necessary for a given util, the value should be an empty object. - */ -const hardcodedColorsToCSSVarsMap = { - 'animate-skeleton-loader': { - '#dcdcde': '--gray-100', - '#ececef': '--gray-50', - }, - 'inset-border-b-2-theme-accent': { - '#6666c4': '--theme-indigo-500', // This gives us `var(--gl-theme-accent, var(--theme-indigo-500, #6666c4))` which I think is good - }, - shadow: {}, // This util already uses hardcoded colors in its legacy version - 'shadow-x0-y2-b4-s0': {}, // This util already uses hardcoded colors in its legacy version - 'shadow-sm': { - '#05050614': '--gl-color-alpha-dark-8', // The dark theme override does not yet exist - }, - 'shadow-md': { - '#05050629': '--gl-color-alpha-dark-6', // The dark theme override does not yet exist - }, - 'shadow-lg': { - '#05050629': '--gl-color-alpha-dark-6', // The dark theme override does not yet exist - }, - 'text-contrast-light': {}, // The legacy util references the $white-contrast variable for which we have no dark theme override - 'text-black-normal': { - '#333': '--gl-text-color-default', - }, - 'text-body': { - '#28272d': '--gl-text-color-default', - }, - 'text-secondary': { - '#737278': '--gl-text-secondary', - }, - 'border-gray-a-08': { - '#05050614': '--gl-color-alpha-dark-8', // The dark theme override does not yet exist - }, - 'inset-border-1-gray-a-08': { - '#05050614': '--gl-color-alpha-dark-8', // The dark theme override does not yet exist - }, - 'border-gray-a-24': { - '#0505063d': '--gl-color-alpha-dark-24', // The dark theme override does not yet exist - }, - border: { - '#dcdcde': '--gl-border-color-default', - }, - 'border-t': { - '#dcdcde': '--gl-border-color-default', - }, - 'border-r': { - '#dcdcde': '--gl-border-color-default', - }, - 'border-b': { - '#dcdcde': '--gl-border-color-default', - }, - 'border-l': { - '#dcdcde': '--gl-border-color-default', - }, - '-focus': { - '#fff': '--white', - '#428fdc': '--blue-400', - }, - 'focus--focus': { - '#fff': '--white', - '#428fdc': '--blue-400', - }, -}; -/** - * Returns a flat array of token entries in the form: - * - * [['#123456','gray-500'],...] - * @param tokens - */ -export function getColorTokens(tokens) { - if (tokens.$type === 'color') { - return [ - [ - // Normalize rgb(a) values to hex values. - tokens.value.startsWith('rgb') ? `#${rgbHex(tokens.value)}` : tokens.value, - tokens.path.join('-'), - ], - ]; - } - if (tokens.$type) { - return []; - } - - return Object.values(tokens).flatMap((t) => getColorTokens(t)); -} - -/** - * Returns a reverse mapping of hex values to tokens, e.g. - * - * { - * '#28272d': [ 'color-neutral-900', 'icon-color-strong', 'gray-900' ] - * } - * - * @param rawTokens - */ -function buildColorToTokenMap(rawTokens) { - const res = {}; - for (const [hex, token] of getColorTokens(rawTokens)) { - res[hex] ||= []; - res[hex].push(token); - // Sort the token names by length because a shorter token name might be part - // of a longer token name. e.g. `something-gray-900` contains `gray-900`. - // But we want to resolve `gl-text-something-gray-900` to something-gray-900 - // and not `gray-900` - res[hex].sort((a, b) => b.length - a.length); - } - return res; -} - -/** - * We get all tokens, but ignore the `text` tokens, - * because the text tokens are correct, semantic tokens, but the values are - * from our gray scale - */ -const { text, ...lightModeTokensRaw } = JSON.parse( - fs.readFileSync(path.join(GITLAB_UI_DIR, 'src/tokens/build/json/tokens.json'), 'utf-8'), -); -const lightModeHexToToken = buildColorToTokenMap(lightModeTokensRaw); - -export const darkModeTokenToHex = Object.fromEntries( - getColorTokens( - JSON.parse( - fs.readFileSync(path.join(GITLAB_UI_DIR, 'src/tokens/build/json/tokens.dark.json'), 'utf-8'), - ), - ).map(([color, key]) => [key.startsWith('text-') ? `gl-${key}` : key, color]), -); - -// We overwrite the following classes in -// app/assets/stylesheets/themes/_dark.scss -darkModeTokenToHex['gl-color-alpha-dark-8'] = '#fbfafd14'; // rgba($gray-950, 0.08); -darkModeTokenToHex['gl-text-secondary'] = '#bfbfc3'; // $gray-700 - -function isImportant(selector) { - return selector.includes('!'); -} - -function getPseudoClass(selector) { - const [, ...state] = selector.split(':'); - return state.length ? `&:${state.join(':')}` : ''; -} - -function getCleanSelector(selector) { - return selector.replace('gl-', '').replace(/:.*/, ''); -} - -/** - * Returns the plain util name from a given selector. - * Essentially removes the leading dot, breakpoint prefix and important suffix if any. - * - * @param {string} cleanSelector The selector from which to extract the util name (should have been cleaned with getCleanSelector first) - */ -function selectorToBaseUtilName(cleanSelector) { - return cleanSelector.replace(/^\.(sm-|md-|lg-)?/, '').replace(/\\!$/, ''); -} - -export const classesWithRawColors = []; - -function normalizeColors(value, cleanSelector) { - return ( - value - // Replace rgb and rgba functions with hex syntax - .replace(/rgba?\([\d ,./]+?\)/g, (rgbaColor) => `#${rgbHex(rgbaColor)}`) - // Find corresponding token for color - .replace(/#(?:[a-f\d]{8}|[a-f\d]{6}|[a-f\d]{4}|[a-f\d]{3})/gi, (hexColor) => { - // transparent rgba hexex - if (hexColor === '#0000' || hexColor === '#00000000') { - return 'transparent'; - } - - // We only want to match a color, - // if the selector contains the color name - const tokenMatch = lightModeHexToToken[hexColor]?.find?.((tokenName) => - cleanSelector.includes(tokenName), - ); - if (tokenMatch) { - return `var(--${tokenMatch}, ${hexColor})`; - } - const utilName = selectorToBaseUtilName(cleanSelector); - const cssVar = hardcodedColorsToCSSVarsMap[utilName]?.[hexColor]; - if (cssVar) { - return `var(${cssVar}, ${hexColor})`; - } - - // Only add this util to the list of hardcoded colors if it was not defined in the - // `hardcodedColorsToCSSVarsMap` map. - if (!hardcodedColorsToCSSVarsMap[utilName]) { - classesWithRawColors.push(cleanSelector); - } - return hexColor; - }) - ); -} - -export function extractRules(css, { convertColors = false } = {}) { - const definitions = {}; - - postcss.parse(css).walkRules((rule) => { - // We skip all atrule, e.g. @keyframe, except @media queries - if (rule.parent?.type === 'atrule' && rule.parent?.name !== 'media') { - console.log(`Skipping atrule of type ${rule.parent?.name}`); - return; - } - - // This is an odd dark-mode only util. We have added it to the dark mode overrides - // and remove it from our utility classes - if (rule.selector.startsWith('.gl-dark .gl-dark-invert-keep-hue')) { - console.log(`Skipping composite selector ${rule.selector} which will be migrated manually`); - return; - } - - // iterate over each class definition - rule.selectors.forEach((selector) => { - let styles = {}; - const cleanSelector = getCleanSelector(selector); - - // iterate over the properties of each class definition - rule.nodes.forEach((node) => { - styles[node.prop] = convertColors ? normalizeColors(node.value, cleanSelector) : node.value; - - if (isImportant(selector)) { - styles[node.prop] += ' !important'; - } - }); - - const pseudoClass = getPseudoClass(selector); - styles = pseudoClass - ? { - [pseudoClass]: styles, - } - : styles; - if (rule.parent?.name === 'media') { - styles = { - [`@media ${rule.parent.params}`]: styles, - }; - } - /* merge existing definitions, because e.g. - .class { - width: 0; - } - @media(...) { - .class { - height: 0; - } - } - needs to merged into: - { '.class': { - 'width': 0; - '@media(...)': { - height: 0; - } - }} - */ - definitions[cleanSelector] = { ...definitions[cleanSelector], ...styles }; - }); - }); - return definitions; -} - -export function normalizeCssInJSDefinition(tailwindDefinition, colorResolver = false) { - if (!tailwindDefinition) { - return null; - } - - // Order property definitions by name. - const ordered = _.pick(tailwindDefinition, Object.keys(tailwindDefinition).sort()); - - return JSON.stringify(ordered, (key, value) => { - if (typeof value === 'string') { - // Normalize decimal values without leading zeroes - // e.g. 0.5px and .5px - if (value.startsWith('0.')) { - return value.substring(1); - } - // Normalize 0px and 0 - if (value === '0px') { - return '0'; - } - - if (colorResolver) { - return colorResolver(value); - } - } - return value; - }); -} diff --git a/scripts/frontend/tailwind_all_the_way.mjs b/scripts/frontend/tailwind_all_the_way.mjs deleted file mode 100755 index 60861c55e16..00000000000 --- a/scripts/frontend/tailwind_all_the_way.mjs +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env node - -/* eslint-disable import/extensions */ - -import { mkdir, readFile, writeFile } from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; -import path from 'node:path'; -import _ from 'lodash'; -import postcss from 'postcss'; -import * as prettier from 'prettier'; - -import tailwindcss from 'tailwindcss/lib/plugin.js'; -import { - classesWithRawColors, - extractRules, - loadCSSFromFile, - mismatchAllowList, - normalizeCssInJSDefinition, -} from './lib/tailwind_migration.mjs'; -import { compileAllStyles } from './lib/compile_css.mjs'; -import { build as buildTailwind } from './tailwindcss.mjs'; - -const PATH_TO_FILE = path.resolve(fileURLToPath(import.meta.url)); -const ROOT_PATH = path.resolve(path.dirname(PATH_TO_FILE), '../../'); -const tempDir = path.join(ROOT_PATH, 'config', 'helpers', 'tailwind'); -const allUtilitiesFile = path.join(tempDir, './all_utilities.haml'); -const tailwindSource = path.join(ROOT_PATH, 'app/assets/stylesheets/tailwind.css'); - -async function writeCssInJs(data) { - const formatted = await prettier.format(data, { - printWidth: 100, - singleQuote: true, - arrowParens: 'always', - trailingComma: 'all', - parser: 'babel', - }); - return writeFile(path.join(tempDir, './css_in_js.js'), formatted, 'utf-8'); -} - -/** - * Writes the CSS in Js in compatibility mode. We write all the utils and we surface things we might - * want to look into (hardcoded colors, definition mismatches). - * - * @param {string} tailwindClasses - * @param {Object} oldUtilityDefinitionsRaw - */ -async function toCompatibilityUtils(tailwindClasses, oldUtilityDefinitionsRaw) { - const oldUtilityDefinitions = _.clone(oldUtilityDefinitionsRaw); - - const tailwindDefinitions = extractRules(tailwindClasses); - - const deleted = []; - const mismatches = []; - - for (const definition of Object.keys(tailwindDefinitions)) { - if ( - mismatchAllowList.includes(definition) || - normalizeCssInJSDefinition(oldUtilityDefinitions[definition]) === - normalizeCssInJSDefinition(tailwindDefinitions[definition]) - ) { - delete oldUtilityDefinitions[definition]; - deleted.push(definition); - } else if (oldUtilityDefinitions[definition]) { - console.log(`Found ${definition} in both, but they don't match:`); - console.log(`\tOld: ${JSON.stringify(oldUtilityDefinitions[definition])}`); - console.log(`\tNew: ${JSON.stringify(tailwindDefinitions[definition])}`); - mismatches.push([ - definition, - oldUtilityDefinitions[definition], - tailwindDefinitions[definition], - ]); - delete oldUtilityDefinitions[definition]; - } - } - - console.log( - `Deleted exact matches:\n\t${_.chunk(deleted, 4) - .map((n) => n.join(' ')) - .join('\n\t')}`, - ); - - const hardcodedColors = _.pick(oldUtilityDefinitions, classesWithRawColors); - const safeToUse = _.omit(oldUtilityDefinitions, classesWithRawColors); - - const stats = { - exactMatches: deleted.length, - potentialMismatches: Object.keys(mismatches).length, - hardcodedColors: Object.keys(hardcodedColors).length, - safeToUseLegacyUtils: Object.keys(safeToUse).length, - }; - - console.log(stats); - - await writeCssInJs( - [ - stats.potentialMismatches && - ` -/* eslint-disable no-unused-vars */ -// The following rules are mismatches between our utility classes and -// tailwinds. So there are two rules in the old system and the new system -// with the same name, but their definitions mismatch. -// The mismatch might be minor, or major and needs to be dealt with manually -// the array below contains: -// [rule name, GitLab UI utility, tailwind utility] -const potentialMismatches = Object.fromEntries( -${JSON.stringify(mismatches, null, 2)} -);`, - stats.hardcodedColors && - ` -// The following definitions have hard-coded colors and do not use -// their var(...) counterparts. We should double-check them and fix them -// manually (e.g. the text- classes should use the text variables and not -// gray-) -const hardCodedColors = ${JSON.stringify(hardcodedColors, null, 2)}; -`, - `module.exports = {`, - stats.hardcodedColors && '...hardCodedColors,', - `...${JSON.stringify(safeToUse, null, 2)}`, - '}', - ] - .filter(Boolean) - .join(''), - { - printWidth: 100, - singleQuote: true, - arrowParens: 'always', - trailingComma: 'all', - parser: 'babel', - }, - ); - - return stats; -} - -/** - * Writes only the style definitions we actually need. - */ -export async function toMinimalUtilities() { - // We re-import the config with a `?minimal` query in order to cache-bust - // the previously loaded config, which doesn't have the latest css_in_js - const { default: tailwindConfig } = await import('../../config/tailwind.config.js?minimal'); - - const { css: tailwindClasses } = await postcss([ - tailwindcss({ - ...tailwindConfig, - // We must ensure the GitLab UI plugin is disabled during this run so that whatever it defines - // is purged out of the CSS-in-Js. - presets: [ - { - ...tailwindConfig.presets[0], - plugins: [], - }, - ], - // Disable all core plugins, all we care about are the legacy utils - // that are provided via addUtilities. - corePlugins: [], - }), - ]).process('@tailwind utilities;', { map: false, from: undefined }); - - const rules = extractRules(tailwindClasses); - - const minimalUtils = Object.keys(rules).length; - - await writeCssInJs(` - /** - * The following ${minimalUtils} definitions need to be migrated to Tailwind. - * Let's do this! 🚀 - */ - module.exports = ${JSON.stringify(rules)}`); - - return { minimalUtils, rules }; -} - -/** - * To run the script in compatibility mode: - * - * ./scripts/frontend/tailwind_all_the_way.mjs - * - * This forces the generation of all possible utilities and surfaces the ones that might require - * further investigation. Once the output has been verified, the script can be re-run in minimal - * mode to only generate the utilities that are used in the product: - * - * ./scripts/frontend/tailwind_all_the_way.mjs --only-used - * - */ -export async function convertUtilsToCSSInJS({ buildOnlyUsed = false } = {}) { - console.log('# Compiling legacy styles'); - - await compileAllStyles({ - style: 'expanded', - filter: (source) => source.includes('application_utilities_to_be_replaced'), - }); - - await mkdir(tempDir, { recursive: true }); - - const oldUtilityDefinitions = extractRules( - loadCSSFromFile('app/assets/builds/application_utilities_to_be_replaced.css'), - { convertColors: true }, - ); - - // Write out all found css classes in order to run tailwind on it. - await writeFile( - allUtilitiesFile, - Object.keys(oldUtilityDefinitions) - .map((clazz) => { - return ( - // Add `gl-` prefix to all classes - `.gl-${clazz.substring(1)}` - // replace the escaped `\!` with ! - .replace(/\\!/g, '!') - ); - }) - .join('\n'), - 'utf-8', - ); - - // Lazily import the tailwind config - const { default: tailwindConfig } = await import('../../config/tailwind.config.js?default'); - - const { css: tailwindClasses } = await postcss([ - tailwindcss({ - ...tailwindConfig, - // We only want to generate the utils based on the fresh - // allUtilitiesFile - content: [allUtilitiesFile], - // We are disabling all plugins to prevent the CSS-in-Js import from causing trouble. - // The GitLab UI preset still registers its own plugin, which we need to define legitimate - // custom utils. - plugins: [], - }), - ]).process(await readFile(tailwindSource, 'utf-8'), { map: false, from: undefined }); - - const stats = await toCompatibilityUtils(tailwindClasses, oldUtilityDefinitions); - - if (buildOnlyUsed) { - console.log('# Reducing utility definitions to minimally used'); - - const { minimalUtils } = await toMinimalUtilities(); - - console.log(`Went from ${stats.safeToUseLegacyUtils} => ${minimalUtils} utility classes`); - } - - await buildTailwind({ content: buildOnlyUsed ? false : allUtilitiesFile }); - - return stats; -} - -if (PATH_TO_FILE.includes(path.resolve(process.argv[1]))) { - console.log('Script called directly.'); - console.log(`CWD${process.cwd()}`); - convertUtilsToCSSInJS({ buildOnlyUsed: process.argv.includes('--only-used') }).catch((e) => { - console.warn(e); - process.exitCode = 1; - }); -} diff --git a/scripts/frontend/tailwind_equivalents.json b/scripts/frontend/tailwind_equivalents.json deleted file mode 100644 index f707c1f3bec..00000000000 --- a/scripts/frontend/tailwind_equivalents.json +++ /dev/null @@ -1,530 +0,0 @@ -{ - "gl-sr-only-focusable": "gl-sr-only focus:gl-not-sr-only", - "gl-spin": "gl-animate-spin", - "gl-animate-skeleton-loader": null, - "gl-hover-bg-transparent": "hover:gl-bg-transparent", - "gl-bg-transparent!": "!gl-bg-transparent", - "gl-hover-bg-transparent!": "hover:!gl-bg-transparent", - "gl-bg-white!": "!gl-bg-white", - "gl-bg-gray-10!": "!gl-bg-gray-10", - "gl-focus-bg-gray-50": "focus:gl-bg-gray-50", - "gl-hover-bg-gray-50": "hover:gl-bg-gray-50", - "gl-bg-gray-50!": "!gl-bg-gray-50", - "gl-bg-gray-100!": "!gl-bg-gray-100", - "gl-bg-gray-900!": "!gl-bg-gray-900", - "gl-hover-bg-blue-50": "hover:gl-bg-blue-50", - "gl-bg-blue-50!": "!gl-bg-blue-50", - "gl-hover-bg-blue-50!": "hover:!gl-bg-blue-50", - "gl-bg-blue-100!": "!gl-bg-blue-100", - "gl-bg-red-200!": "!gl-bg-red-200", - "gl-bg-t-gray-a-08": "gl-bg-alpha-dark-8", - "gl-hover-bg-t-gray-a-08": "hover:gl-bg-alpha-dark-8", - "gl-bg-none": "gl-bg-transparent", - "gl-bg-size-cover": "gl-bg-cover", - "gl-border!": "!gl-border", - "gl-border-l!": "!gl-border-l", - "gl-border-none!": "!gl-border-none", - "gl-border-t-none": "gl-border-t-0", - "gl-border-t-none!": "!gl-border-t-0", - "gl-border-solid!": "!gl-border-solid", - "gl-hover-border-b-solid": "hover:gl-border-b-solid", - "gl-border-b-solid!": "!gl-border-b-solid", - "gl-border-t-transparent!": "!gl-border-t-transparent", - "gl-hover-border-gray-100": "hover:gl-border-gray-100", - "gl-border-gray-100!": "!gl-border-gray-100", - "gl-hover-border-gray-200": "hover:gl-border-gray-200", - "gl-border-gray-200!": "!gl-border-gray-200", - "gl-hover-border-gray-200!": "hover:!gl-border-gray-200", - "gl-border-gray-500!": "!gl-border-gray-500", - "gl-border-red-500!": "!gl-border-red-500", - "gl-hover-border-blue-200": "hover:gl-border-blue-200", - "gl-border-blue-600!": "!gl-border-blue-600", - "gl-border-gray-a-08": "gl-border-alpha-dark-8", - "gl-border-gray-a-24": "gl-border-alpha-dark-24", - "gl-border-t-gray-200!": "!gl-border-t-gray-200", - "gl-border-r-gray-200!": "!gl-border-r-gray-200", - "gl-border-b-gray-100!": "!gl-border-b-gray-100", - "gl-border-0!": "!gl-border-0", - "gl-border-t-0!": "!gl-border-t-0", - "gl-border-b-0!": "!gl-border-b-0", - "gl-border-l-0!": "!gl-border-l-0", - "gl-border-r-0!": "!gl-border-r-0", - "gl-border-1!": "!gl-border-1", - "gl-border-t-1!": "!gl-border-t-1", - "gl-border-b-1!": "!gl-border-b-1", - "gl-border-b-2!": "!gl-border-b-2", - "gl-border-top-0": "gl-border-t-0", - "gl-border-top-0!": "!gl-border-t-0", - "gl-border-bottom-0": "gl-border-b-0", - "gl-border-bottom-0!": "!gl-border-b-0", - "gl-rounded-0": "gl-rounded-none", - "gl-rounded-0!": "!gl-rounded-none", - "gl-rounded-base!": "!gl-rounded-base", - "gl-rounded-full!": "!gl-rounded-full", - "gl-rounded-lg!": "!gl-rounded-lg", - "gl-rounded-left-none!": "!gl-rounded-l-none", - "gl-rounded-top-left-base": "gl-rounded-tl-base", - "gl-rounded-top-left-none": "gl-rounded-tl-none", - "gl-rounded-top-left-none!": "!gl-rounded-tl-none", - "gl-rounded-top-right-base": "gl-rounded-tr-base", - "gl-rounded-top-right-base!": "!gl-rounded-tr-base", - "gl-rounded-top-right-none": "gl-rounded-tr-none", - "gl-rounded-top-right-none!": "!gl-rounded-tr-none", - "gl-rounded-top-base": "gl-rounded-t-base", - "gl-rounded-bottom-left-small": "gl-rounded-bl-small", - "gl-rounded-bottom-left-base": "gl-rounded-bl-base", - "gl-rounded-bottom-left-base!": "!gl-rounded-bl-base", - "gl-rounded-bottom-left-none": "gl-rounded-bl-none", - "gl-rounded-bottom-left-none!": "!gl-rounded-bl-none", - "gl-rounded-bottom-right-small": "gl-rounded-br-small", - "gl-rounded-bottom-right-base": "gl-rounded-br-base", - "gl-rounded-bottom-right-base!": "!gl-rounded-br-base", - "gl-rounded-bottom-right-none": "gl-rounded-br-none", - "gl-rounded-bottom-right-none!": "!gl-rounded-br-none", - "gl-rounded-bottom-base": "gl-rounded-b-base", - "gl-rounded-top-left-small": "gl-rounded-tl-small", - "gl-rounded-top-right-small": "gl-rounded-tr-small", - "gl-inset-border-1-gray-100!": "!gl-shadow-inner-1-gray-100", - "gl-inset-border-1-gray-400": "gl-shadow-inner-1-gray-400", - "gl-inset-border-1-gray-400!": "!gl-shadow-inner-1-gray-400", - "gl-focus-inset-border-2-blue-400!": "focus:!gl-shadow-inner-2-blue-400", - "gl-inset-border-b-2-blue-500": "gl-shadow-inner-b-2-blue-500", - "gl-inset-border-1-red-500!": "!gl-shadow-inner-1-red-500", - "gl-shadow-none!": "!gl-shadow-none", - "gl-clearfix!": "!gl-clearfix", - "gl-reset-color": "gl-text-inherit", - "gl-reset-color!": "!gl-text-inherit", - "gl-text-white!": "!gl-text-white", - "gl-text-body": "gl-text-primary", - "gl-text-body!": "!gl-text-primary", - "gl-text-secondary!": "!gl-text-secondary", - "gl-sm-text-body": "sm:gl-text-primary", - "gl-text-black-normal": "gl-text-default", - "gl-text-black-normal!": "!gl-text-default", - "gl-text-gray-300!": "!gl-text-gray-300", - "gl-text-gray-400!": "!gl-text-gray-400", - "gl-text-gray-500!": "!gl-text-gray-500", - "gl-text-gray-700!": "!gl-text-gray-700", - "gl-focus-text-gray-900": "focus:gl-text-gray-900", - "gl-hover-text-gray-900": "hover:gl-text-gray-900", - "gl-text-gray-900!": "!gl-text-gray-900", - "gl-hover-text-gray-900!": "hover:!gl-text-gray-900", - "gl-text-blue-500!": "!gl-text-blue-500", - "gl-hover-text-blue-600": "hover:gl-text-blue-600", - "gl-text-blue-600!": "!gl-text-blue-600", - "gl-hover-text-blue-800": "hover:gl-text-blue-800", - "gl-hover-text-blue-800!": "hover:!gl-text-blue-800", - "gl-text-red-500!": "!gl-text-red-500", - "gl--flex-center": "gl-flex gl-items-center gl-justify-center", - "gl-focus--focus": "focus:gl-focus", - "gl-cursor-default!": "!gl-cursor-default", - "gl-hover-cursor-pointer": "hover:gl-cursor-pointer", - "gl-cursor-grabbing!": "!gl-cursor-grabbing", - "gl-hover-cursor-not-allowed!": "hover:!gl-cursor-not-allowed", - "gl-cursor-text!": "!gl-cursor-text", - "gl-hover-cursor-crosshair": "hover:gl-cursor-crosshair", - "gl-cursor-help!": "!gl-cursor-help", - "gl-deprecated-top-66vh": null, - "gl-number-as-text-input": "gl-no-spin", - "gl-display-none": "gl-hidden", - "gl-display-none!": "!gl-hidden", - "gl-sm-display-none": "sm:gl-hidden", - "gl-sm-display-none!": "sm:!gl-hidden", - "gl-md-display-none": "md:gl-hidden", - "gl-md-display-none!": "md:!gl-hidden", - "gl-lg-display-none": "lg:gl-hidden", - "gl-lg-display-none!": "lg:!gl-hidden", - "gl-display-flex": "gl-flex", - "gl-display-flex!": "!gl-flex", - "gl-sm-display-flex": "sm:gl-flex", - "gl-sm-display-flex!": "sm:!gl-flex", - "gl-md-display-flex": "md:gl-flex", - "gl-md-display-flex!": "md:!gl-flex", - "gl-lg-display-flex": "lg:gl-flex", - "gl-display-inline-flex": "gl-inline-flex", - "gl-display-inline-flex!": "!gl-inline-flex", - "gl-sm-display-inline-flex": "sm:gl-inline-flex", - "gl-sm-display-inline-flex!": "sm:!gl-inline-flex", - "gl-md-display-inline-flex": "md:gl-inline-flex", - "gl-md-display-inline-flex!": "md:!gl-inline-flex", - "gl-lg-display-inline-flex": "lg:gl-inline-flex", - "gl-display-block": "gl-block", - "gl-display-block!": "!gl-block", - "gl-sm-display-block": "sm:gl-block", - "gl-sm-display-block!": "sm:!gl-block", - "gl-md-display-block": "md:gl-block", - "gl-md-display-block!": "md:!gl-block", - "gl-lg-display-block": "lg:gl-block", - "gl-lg-display-block!": "lg:!gl-block", - "gl-display-inline": "gl-inline", - "gl-display-inline!": "!gl-inline", - "gl-sm-display-inline": "sm:gl-inline", - "gl-md-display-inline": "md:gl-inline", - "gl-display-inline-block": "gl-inline-block", - "gl-sm-display-inline-block": "sm:gl-inline-block", - "gl-sm-display-inline-block!": "sm:!gl-inline-block", - "gl-md-display-inline-block": "md:gl-inline-block", - "gl-md-display-inline-block!": "md:!gl-inline-block", - "gl-lg-display-inline-block": "lg:gl-inline-block", - "gl-display-table": "gl-table", - "gl-display-table-row": "gl-table-row", - "gl-display-table-row!": "!gl-table-row", - "gl-display-table-cell": "gl-table-cell", - "gl-display-table-cell!": "!gl-table-cell", - "gl-display-grid": "gl-grid", - "gl-sm-display-table-cell!": "sm:!gl-table-cell", - "gl-md-display-table-cell": "md:gl-table-cell", - "gl-lg-display-table-cell!": "lg:!gl-table-cell", - "gl-display-contents": "gl-contents", - "gl-align-items-baseline": "gl-items-baseline", - "gl-align-items-center": "gl-items-center", - "gl-align-items-center!": "!gl-items-center", - "gl-align-items-flex-start": "gl-items-start", - "gl-align-items-flex-start!": "!gl-items-start", - "gl-align-items-flex-end": "gl-items-end", - "gl-align-items-stretch": "gl-items-stretch", - "gl-align-items-stretch!": "!gl-items-stretch", - "gl-sm-align-items-center": "sm:gl-items-center", - "gl-md-align-items-center": "md:gl-items-center", - "gl-lg-align-items-center": "lg:gl-items-center", - "gl-sm-align-items-flex-start": "sm:gl-items-start", - "gl-sm-align-items-flex-end": "sm:gl-items-end", - "gl-md-align-items-flex-start": "md:gl-items-start", - "gl-lg-align-items-flex-start": "lg:gl-items-start", - "gl-lg-align-items-flex-end": "lg:gl-items-end", - "gl-sm-flex-wrap": "sm:gl-flex-wrap", - "gl-md-flex-nowrap": "md:gl-flex-nowrap", - "gl-sm-flex-nowrap": "sm:gl-flex-nowrap", - "gl-flex-direction-column": "gl-flex-col", - "gl-md-flex-direction-column": "md:gl-flex-col", - "gl-md-flex-direction-column!": "md:!gl-flex-col", - "gl-lg-flex-direction-column!": "lg:!gl-flex-col", - "gl-flex-direction-column-reverse": "gl-flex-col-reverse", - "gl-flex-direction-row": "gl-flex-row", - "gl-flex-direction-row!": "!gl-flex-row", - "gl-sm-flex-direction-row": "sm:gl-flex-row", - "gl-sm-flex-direction-row!": "sm:!gl-flex-row", - "gl-md-flex-direction-row": "md:gl-flex-row", - "gl-lg-flex-direction-row": "lg:gl-flex-row", - "gl-lg-flex-direction-row!": "lg:!gl-flex-row", - "gl-xl-flex-direction-row": "xl:gl-flex-row", - "gl-flex-direction-row-reverse": "gl-flex-row-reverse", - "gl-sm-flex-direction-row-reverse": "sm:gl-flex-row-reverse", - "gl-md-flex-direction-row-reverse": "md:gl-flex-row-reverse", - "gl-flex-grow-0!": "!gl-grow-0", - "gl-flex-grow-1": "gl-grow", - "gl-flex-shrink-0": "gl-shrink-0", - "gl-md-flex-grow-0": "md:gl-grow-0", - "gl-flex-basis-0": "gl-basis-0", - "gl-flex-basis-quarter": "gl-basis-1/4", - "gl-flex-basis-third": "gl-basis-1/3", - "gl-md-flex-basis-13": "md:gl-basis-13", - "gl-flex-basis-two-thirds": "gl-basis-2/3", - "gl-flex-basis-half": "gl-basis-1/2", - "gl-flex-basis-full": "gl-basis-full", - "gl-flex-basis-full!": "!gl-basis-full", - "gl-flex-flow-row-wrap": "gl-flex-row gl-flex-wrap", - "gl-justify-content-center": "gl-justify-center", - "gl-justify-content-end": "gl-justify-end", - "gl-justify-content-end!": "!gl-justify-end", - "gl-sm-justify-content-end": "sm:gl-justify-end", - "gl-md-justify-content-center!": "md:!gl-justify-center", - "gl-md-justify-content-end": "md:gl-justify-end", - "gl-lg-justify-content-end": "lg:gl-justify-end", - "gl-justify-content-space-between": "gl-justify-between", - "gl-md-justify-content-space-between": "md:gl-justify-between", - "gl-justify-content-start": "gl-justify-start", - "gl-justify-content-start!": "!gl-justify-start", - "gl-sm-justify-content-start": "sm:gl-justify-start", - "gl-md-justify-content-start": "md:gl-justify-start", - "gl-lg-justify-content-start": "lg:gl-justify-start", - "gl-align-self-start": "gl-self-start", - "gl-align-self-end": "gl-self-end", - "gl-align-self-center": "gl-self-center", - "gl-md-align-self-center": "md:gl-self-center", - "gl-align-self-baseline": "gl-self-baseline", - "gl-sm-grid-template-columns-2": "sm:gl-grid-cols-2", - "gl-md-grid-template-columns-2": "md:gl-grid-cols-2", - "gl-lg-grid-template-columns-4": "lg:gl-grid-cols-4", - "gl-list-style-none": "gl-list-none", - "gl-opacity-0!": "!gl-opacity-0", - "gl-outline-0": "gl-outline-none", - "gl-outline-0!": "!gl-outline-none", - "gl-outline-none!": "!gl-outline-none", - "gl-overflow-hidden!": "!gl-overflow-hidden", - "gl-overflow-x-hidden!": "!gl-overflow-x-hidden", - "gl-overflow-wrap-break": "gl-break-words", - "gl-overflow-wrap-anywhere": "gl-break-anywhere", - "gl-overflow-visible!": "!gl-overflow-visible", - "gl-w-auto!": "!gl-w-auto", - "gl-w-31!": "!gl-w-31", - "gl-w-10p": "gl-w-1/10", - "gl-w-40p": "gl-w-4/10", - "gl-w-90p": "gl-w-9/10", - "gl-md-w-full": "md:gl-w-full", - "gl-h-auto!": "!gl-h-auto", - "gl-h-6!": "!gl-h-6", - "gl-h-7!": "!gl-h-7", - "gl-h-full!": "!gl-h-full", - "gl-sm-w-auto": "sm:gl-w-auto", - "gl-sm-w-half": "sm:gl-w-1/2", - "gl-sm-w-25p": "sm:gl-w-1/4", - "gl-sm-w-30p": "sm:gl-w-3/10", - "gl-sm-w-40p": "sm:gl-w-4/10", - "gl-sm-w-75p": "sm:gl-w-3/4", - "gl-md-w-15": "md:gl-w-15", - "gl-md-w-20": "md:gl-w-20", - "gl-md-w-30": "md:gl-w-30", - "gl-md-w-half": "md:gl-w-1/2", - "gl-lg-w-half": "lg:gl-w-1/2", - "gl-md-w-auto": "md:gl-w-auto", - "gl-md-w-50p": "md:gl-w-1/2", - "gl-lg-w-1px": "lg:gl-w-px", - "gl-lg-w-auto": "lg:gl-w-auto", - "gl-lg-w-25p": "lg:gl-w-1/4", - "gl-lg-w-30p": "lg:gl-w-3/10", - "gl-lg-w-40p": "lg:gl-w-4/10", - "gl-min-w-full!": "!gl-min-w-full", - "gl-min-w-fit-content!": "!gl-min-w-fit", - "gl-min-h-6!": "!gl-min-h-6", - "gl-min-h-7!": "!gl-min-h-7", - "gl-max-w-20!": "!gl-max-w-20", - "gl-max-w-30!": "!gl-max-w-30", - "gl-max-w-none!": "!gl-max-w-none", - "gl-max-w-full!": "!gl-max-w-full", - "gl-max-h-full!": "!gl-max-h-full", - "gl-max-w-50p": "gl-max-w-1/2", - "gl-md-max-w-26": "md:gl-max-w-26", - "gl-md-max-w-15p": "md:gl-max-w-3/20", - "gl-md-max-w-30p": "md:gl-max-w-3/10", - "gl-md-max-w-50p": "md:gl-max-w-1/2", - "gl-md-max-w-70p": "md:gl-max-w-7/10", - "gl-lg-max-w-80p": "lg:gl-max-w-8/10", - "gl-p-0!": "!gl-p-0", - "gl-p-1!": "!gl-p-1", - "gl-p-2!": "!gl-p-2", - "gl-p-3!": "!gl-p-3", - "gl-p-4!": "!gl-p-4", - "gl-p-5!": "!gl-p-5", - "gl-px-0!": "!gl-px-0", - "gl-px-2!": "!gl-px-2", - "gl-px-3!": "!gl-px-3", - "gl-px-4!": "!gl-px-4", - "gl-px-5!": "!gl-px-5", - "gl-px-9!": "!gl-px-9", - "gl-pr-0!": "!gl-pr-0", - "gl-pr-2!": "!gl-pr-2", - "gl-pr-3!": "!gl-pr-3", - "gl-pr-4!": "!gl-pr-4", - "gl-pr-7!": "!gl-pr-7", - "gl-pr-8!": "!gl-pr-8", - "gl-pr-9!": "!gl-pr-9", - "gl-pl-0!": "!gl-pl-0", - "gl-pl-2!": "!gl-pl-2", - "gl-pl-3!": "!gl-pl-3", - "gl-pl-5!": "!gl-pl-5", - "gl-pl-6!": "!gl-pl-6", - "gl-pl-7!": "!gl-pl-7", - "gl-pl-9!": "!gl-pl-9", - "gl-pt-0!": "!gl-pt-0", - "gl-pt-2!": "!gl-pt-2", - "gl-pt-3!": "!gl-pt-3", - "gl-pt-4!": "!gl-pt-4", - "gl-pt-5!": "!gl-pt-5", - "gl-pt-6!": "!gl-pt-6", - "gl-pb-0!": "!gl-pb-0", - "gl-pb-1!": "!gl-pb-1", - "gl-pb-2!": "!gl-pb-2", - "gl-pb-3!": "!gl-pb-3", - "gl-pb-4!": "!gl-pb-4", - "gl-pb-5!": "!gl-pb-5", - "gl-pb-6!": "!gl-pb-6", - "gl-py-0!": "!gl-py-0", - "gl-py-2!": "!gl-py-2", - "gl-py-3!": "!gl-py-3", - "gl-py-4!": "!gl-py-4", - "gl-py-5!": "!gl-py-5", - "gl-py-6!": "!gl-py-6", - "gl-m-0!": "!gl-m-0", - "gl-mt-0!": "!gl-mt-0", - "gl-mt-n1": "-gl-mt-1", - "gl-mt-2!": "!gl-mt-2", - "gl-mt-n2": "-gl-mt-2", - "gl-mt-3!": "!gl-mt-3", - "gl-mt-4!": "!gl-mt-4", - "gl-mt-5!": "!gl-mt-5", - "gl-mt-6!": "!gl-mt-6", - "gl-mr-0!": "!gl-mr-0", - "gl-mr-2!": "!gl-mr-2", - "gl-mr-3!": "!gl-mr-3", - "gl-mr-n3": "-gl-mr-3", - "gl-mr-4!": "!gl-mr-4", - "gl-sm-mr-3": "sm:gl-mr-3", - "gl-mb-0!": "!gl-mb-0", - "gl-mb-1!": "!gl-mb-1", - "gl-mb-n1": "-gl-mb-1", - "gl-mb-2!": "!gl-mb-2", - "gl-mb-n2": "-gl-mb-2", - "gl-mb-3!": "!gl-mb-3", - "gl-mb-n3": "-gl-mb-3", - "gl-mb-n3!": "!-gl-mb-3", - "gl-mb-4!": "!gl-mb-4", - "gl-mb-5!": "!gl-mb-5", - "gl-mb-6!": "!gl-mb-6", - "gl-sm-ml-auto": "sm:gl-ml-auto", - "gl-ml-0!": "!gl-ml-0", - "gl-ml-n1": "-gl-ml-1", - "gl-ml-2!": "!gl-ml-2", - "gl-ml-n2": "-gl-ml-2", - "gl-ml-3!": "!gl-ml-3", - "gl-ml-n3": "-gl-ml-3", - "gl-ml-n4": "-gl-ml-4", - "gl-ml-n4!": "!-gl-ml-4", - "gl-my-0!": "!gl-my-0", - "gl-my-2!": "!gl-my-2", - "gl-my-4!": "!gl-my-4", - "gl-mx-auto!": "!gl-mx-auto", - "gl-mx-0!": "!gl-mx-0", - "gl-mx-1!": "!gl-mx-1", - "gl-mx-2!": "!gl-mx-2", - "gl-mx-3!": "!gl-mx-3", - "gl-my-n1": "-gl-my-1", - "gl-mx-n1": "-gl-mx-1", - "gl-my-n2": "-gl-my-2", - "gl-my-n2!": "!-gl-my-2", - "gl-mx-n2": "-gl-mx-2", - "gl-my-n3": "-gl-my-3", - "gl-my-n3!": "!-gl-my-3", - "gl-mx-n3": "-gl-mx-3", - "gl-mx-n4": "-gl-mx-4", - "gl-mx-n5": "-gl-mx-5", - "gl-sm-gap-3": "sm:gl-gap-3", - "gl-sm-ml-3": "sm:gl-ml-3", - "gl-sm-ml-3!": "sm:!gl-ml-3", - "gl-sm-ml-5": "sm:gl-ml-5", - "gl-sm-ml-7": "sm:gl-ml-7", - "gl-sm-mr-0": "sm:gl-mr-0", - "gl-sm-mt-0": "sm:gl-mt-0", - "gl-sm-mt-6!": "sm:!gl-mt-6", - "gl-sm-mb-0": "sm:gl-mb-0", - "gl-sm-mb-0!": "sm:!gl-mb-0", - "gl-sm-mb-7": "sm:gl-mb-7", - "gl-sm-mx-0": "sm:gl-mx-0", - "gl-md-mt-0": "md:gl-mt-0", - "gl-md-mt-5": "md:gl-mt-5", - "gl-md-mb-0": "md:gl-mb-0", - "gl-md-mb-0!": "md:!gl-mb-0", - "gl-md-mb-3!": "md:!gl-mb-3", - "gl-lg-mb-0": "lg:gl-mb-0", - "gl-lg-mb-5": "lg:gl-mb-5", - "gl-md-ml-auto": "md:gl-ml-auto", - "gl-md-ml-2": "md:gl-ml-2", - "gl-md-ml-3": "md:gl-ml-3", - "gl-md-mr-3": "md:gl-mr-3", - "gl-md-mr-5": "md:gl-mr-5", - "gl-lg-mr-3": "lg:gl-mr-3", - "gl-lg-mr-10": "lg:gl-mr-10", - "gl-lg-mr-12": "lg:gl-mr-12", - "gl-lg-ml-2": "lg:gl-ml-2", - "gl-lg-ml-3": "lg:gl-ml-3", - "gl-lg-ml-10": "lg:gl-ml-10", - "gl-lg-ml-12": "lg:gl-ml-12", - "gl-xl-ml-3": "xl:gl-ml-3", - "gl-lg-mx-3": "lg:gl-mx-3", - "gl-lg-mx-12": "lg:gl-mx-12", - "gl-lg-my-5": "lg:gl-my-5", - "gl-lg-mt-0": "lg:gl-mt-0", - "gl-lg-mt-5": "lg:gl-mt-5", - "gl-sm-pr-2": "sm:gl-pr-2", - "gl-sm-pr-4": "sm:gl-pr-4", - "gl-sm-pl-6": "sm:gl-pl-6", - "gl-md-pt-0": "md:gl-pt-0", - "gl-md-pt-2": "md:gl-pt-2", - "gl-md-pt-3": "md:gl-pt-3", - "gl-md-pt-11!": "md:!gl-pt-11", - "gl-md-pr-0": "md:gl-pr-0", - "gl-md-pr-0!": "md:!gl-pr-0", - "gl-md-pr-3": "md:gl-pr-3", - "gl-md-pr-5": "md:gl-pr-5", - "gl-md-pl-0": "md:gl-pl-0", - "gl-md-pl-0!": "md:!gl-pl-0", - "gl-md-pl-3": "md:gl-pl-3", - "gl-md-pl-5": "md:gl-pl-5", - "gl-md-pl-7": "md:gl-pl-7", - "gl-md-px-7": "md:gl-px-7", - "gl-lg-pt-3": "lg:gl-pt-3", - "gl-lg-pr-5": "lg:gl-pr-5", - "gl-text-left!": "!gl-text-left", - "gl-text-center!": "!gl-text-center", - "gl-reset-text-align": "gl-text-align-inherit", - "gl-reset-text-align!": "!gl-text-align-inherit", - "gl-text-decoration-none": "gl-no-underline", - "gl-active-text-decoration-none": "active:gl-no-underline", - "gl-focus-text-decoration-none": "focus:gl-no-underline", - "gl-hover-text-decoration-none": "hover:gl-no-underline", - "gl-text-decoration-none!": "!gl-no-underline", - "gl-active-text-decoration-none!": "active:!gl-no-underline", - "gl-hover-text-decoration-none!": "hover:!gl-no-underline", - "gl-text-decoration-underline": "gl-underline", - "gl-hover-text-decoration-underline": "hover:gl-underline", - "gl-text-transform-capitalize": "gl-capitalize", - "gl-text-transform-uppercase": "gl-uppercase", - "gl-text-overflow-ellipsis": "gl-text-ellipsis", - "gl-text-overflow-ellipsis!": "!gl-text-ellipsis", - "gl-white-space-normal": "gl-whitespace-normal", - "gl-white-space-normal!": "!gl-whitespace-normal", - "gl-white-space-nowrap": "gl-whitespace-nowrap", - "gl-white-space-pre-wrap": "gl-whitespace-pre-wrap", - "gl-white-space-pre-wrap!": "!gl-whitespace-pre-wrap", - "gl-white-space-pre-line": "gl-whitespace-pre-line", - "gl-word-break-word": "gl-break-anywhere", - "gl-text-truncate": "gl-truncate", - "gl-translate-y-n100": "-gl-translate-y-full", - "gl-transition-duration-slow": "gl-duration-slow", - "gl-transition-duration-medium": "gl-duration-medium", - "gl-transition-timing-function-ease": "gl-ease-ease", - "gl-transition-medium": "gl-transition-all", - "gl-font-style-italic": "gl-italic", - "gl-font-sm": "gl-text-sm", - "gl-font-sm!": "!gl-text-sm", - "gl-font-base": "gl-text-base", - "gl-font-lg": "gl-text-lg", - "gl-font-lg!": "!gl-text-lg", - "gl-font-size-h-display": "gl-text-size-h-display", - "gl-font-size-h1": "gl-text-size-h1", - "gl-font-size-h2": "gl-text-size-h2", - "gl-font-size-h1-xl": "gl-text-size-h1-xl", - "gl-font-size-h2-xl": "gl-text-size-h2-xl", - "gl-font-size-markdown": "gl-text-lg", - "gl-reset-font-size": "gl-text-size-reset", - "gl-font-weight-100": "gl-font-100", - "gl-font-weight-300": "gl-font-300", - "gl-font-weight-normal": "gl-font-normal", - "gl-font-weight-normal!": "!gl-font-normal", - "gl-font-weight-semibold": "gl-font-semibold", - "gl-font-weight-semibold!": "!gl-font-semibold", - "gl-font-weight-bold": "gl-font-bold", - "gl-font-weight-bold!": "!gl-font-bold", - "gl-sm-font-weight-bold": "sm:gl-font-bold", - "gl-line-height-0": "gl-leading-0", - "gl-line-height-1": "gl-leading-1", - "gl-line-height-ratio-1000": "gl-leading-1", - "gl-line-height-normal": "gl-leading-normal", - "gl-line-height-normal!": "!gl-leading-normal", - "gl-line-height-20": "gl-leading-20", - "gl-line-height-20!": "!gl-leading-20", - "gl-line-height-24": "gl-leading-24", - "gl-line-height-28": "gl-leading-28", - "gl-line-height-28!": "!gl-leading-28", - "gl-line-height-32": "gl-leading-32", - "gl-line-height-36": "gl-leading-36", - "gl-line-height-42": "gl-leading-42", - "gl-vertical-align-top": "gl-align-top", - "gl-vertical-align-middle": "gl-align-middle", - "gl-vertical-align-bottom": "gl-align-bottom", - "gl-vertical-align-text-bottom": "gl-align-text-bottom", - "gl-vertical-align-text-bottom!": "!gl-align-text-bottom", - "gl-visibility-hidden": "gl-invisible", - "gl-z-index-200": "gl-z-200", - "gl-z-index-9999!": "!gl-z-9999" -} diff --git a/scripts/frontend/tailwind_lint_against_legacy_utils.js b/scripts/frontend/tailwind_lint_against_legacy_utils.js new file mode 100755 index 00000000000..99f903896bb --- /dev/null +++ b/scripts/frontend/tailwind_lint_against_legacy_utils.js @@ -0,0 +1,220 @@ +#!/usr/bin/env node + +/* eslint-disable import/extensions */ + +const { readFile } = require('node:fs/promises'); +const path = require('node:path'); +const _ = require('lodash'); +const postcss = require('postcss'); +const tailwindPlugin = require('tailwindcss/plugin.js'); +const tailwindcss = require('tailwindcss/lib/plugin.js'); +const tailwindConfig = require('../../config/tailwind.config.js'); + +const ROOT_PATH = path.resolve(__dirname, '../../'); +const tailwindSource = path.join(ROOT_PATH, 'app/assets/stylesheets/tailwind.css'); +const legacyUtilsSource = path.join(ROOT_PATH, 'node_modules/@gitlab/ui/dist/utility_classes.css'); + +/** + * Strips trailing modifiers like `:hover`, `::before` from selectors, + * only returning the class name + * + * For example: .gl-foo-hover-bar:hover will be turned into: .gl-foo-bar + * @param {String} selector + * @returns {String} + */ +function getCleanSelector(selector) { + return selector.replace(/:.*/, ''); +} + +/** + * Extracts all class names from a CSS file + * + * @param {String} css + * @returns {Set} + */ +function extractClassNames(css) { + const definitions = new Set(); + + postcss.parse(css).walkRules((rule) => { + // We skip all atrule, e.g. @keyframe, except @media queries + if (rule.parent?.type === 'atrule' && rule.parent?.name !== 'media') { + console.log(`Skipping atrule of type ${rule.parent?.name}`); + return; + } + + // This is an odd dark-mode only util. We have added it to the dark mode overrides + // and remove it from our utility classes + if (rule.selector.startsWith('.gl-dark .gl-dark-invert-keep-hue')) { + console.log(`Skipping composite selector ${rule.selector} which will be migrated manually`); + return; + } + + // iterate over each class definition + rule.selectors.forEach((selector) => { + definitions.add(getCleanSelector(selector)); + }); + }); + + return definitions; +} + +/** + * Writes the CSS in Js in compatibility mode. We write all the utils and we surface things we might + * want to look into (hardcoded colors, definition mismatches). + * + * @param {Set} tailwindClassNames + * @param {Set} oldClassNames + */ +async function compareLegacyClassesWithTailwind(tailwindClassNames, oldClassNames) { + const oldUtilityNames = new Set(oldClassNames); + + const deleted = new Set(); + + for (const definition of tailwindClassNames) { + if (oldUtilityNames.has(definition)) { + oldUtilityNames.delete(definition); + deleted.add(definition); + } + } + + console.log( + `Legacy classes which have a tailwind equivalent:\n\t${_.chunk(Array.from(deleted), 4) + .map((n) => n.join(' ')) + .join('\n\t')}`, + ); + + return { oldUtilityNames }; +} + +/** + * Runs tailwind on the whole code base, but with mock utilities only. + * + * We hand in a Set of class names (e.g. `.foo-bar`, `.bar-baz`) and tailwind will run + * if one of our source files contains e.g. `.gl-foo-bar` or `.gl-bar-baz`, + * it will be returned + * + * @param {Set} oldClassNames + * @param {Array} content + * @returns {Promise<{rules: Set}>} + */ +async function toMinimalUtilities(oldClassNames, content = []) { + const { css: tailwindClasses } = await postcss([ + tailwindcss({ + ...tailwindConfig, + content: Array.isArray(content) && content.length > 0 ? content : tailwindConfig.content, + // We must ensure the GitLab UI plugin is disabled during this run so that whatever it defines + // is purged out of the CSS-in-Js. + presets: [ + { + ...tailwindConfig.presets[0], + plugins: [], + }, + ], + // Disable all core plugins, all we care about are the legacy utils + // that are provided via addUtilities. + corePlugins: [], + plugins: [ + tailwindPlugin(({ addUtilities }) => { + addUtilities( + Object.fromEntries( + Array.from(oldClassNames).map((className) => [ + // Strip leading `.gl-` because tailwind will add the prefix itself + className.replace(/^\.gl-/, '.'), + { width: 0 }, + ]), + ), + ); + }), + ], + }), + ]).process('@tailwind utilities;', { map: false, from: undefined }); + + const rules = tailwindClasses + .replace(/@.+?{([\s\S]+?)}/gim, '$1') + .replace(/\{[\s\S]+?}/gim, '') + .split('\n') + .map((x) => x.trim()) + .filter(Boolean); + + return { rules: new Set(rules) }; +} + +async function lintAgainstLegacyUtils({ content = [] } = {}) { + console.log('# Checking whether legacy GitLab utility classes are used'); + + console.log('## Extracting legacy util class names'); + + const legacyClassNames = extractClassNames(await readFile(legacyUtilsSource, 'utf-8')); + + /** + * Document containing all utilities at least once, like this: + * + *
+ *
+ * @type {string} + */ + const allLegacyDocument = Array.from(legacyClassNames) + .map((className) => { + const cleanClass = className + .substring(1) + // replace escaped `\!` with ! + .replace(/\\!/g, '!'); + + return `
`; + }) + .join('\n'); + + const { css } = await postcss([ + tailwindcss({ + ...tailwindConfig, + content: [{ raw: allLegacyDocument, extension: 'html' }], + // We are disabling all plugins to prevent the CSS-in-Js import from causing trouble. + // The GitLab UI preset still registers its own plugin, which we need to define legitimate + // custom utils. + plugins: [], + }), + ]).process(await readFile(tailwindSource, 'utf-8'), { map: false, from: undefined }); + + const tailwindClassNames = extractClassNames(css); + + console.log('## Comparing legacy utils to current tailwind class names'); + + const { oldUtilityNames } = await compareLegacyClassesWithTailwind( + tailwindClassNames, + legacyClassNames, + ); + + console.log('## Checking whether a legacy class name is used'); + + const { rules } = await toMinimalUtilities(oldUtilityNames, content); + + console.log(`Went from ${oldUtilityNames.size} => ${rules.size} utility classes`); + + if (rules.size > 0) { + const message = `You are introducing legacy utilities: +\t${Array.from(rules).sort().join('\n\t')} +Please migrate them to tailwind utilities: +https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/doc/tailwind-migration.md`; + throw new Error(message); + } +} + +function wasScriptCalledDirectly() { + return process.argv[1] === __filename; +} + +if (wasScriptCalledDirectly()) { + lintAgainstLegacyUtils() + .then(() => { + console.log('# All good – Happiness. May the tailwind boost your journey'); + }) + .catch((e) => { + console.warn('An error happened'); + console.warn(e.message); + process.exitCode = 1; + }); +} + +module.exports = { + lintAgainstLegacyUtils, +}; diff --git a/scripts/frontend/tailwindcss.mjs b/scripts/frontend/tailwindcss.mjs index 45dfaf1fc3d..5932b3c20a0 100644 --- a/scripts/frontend/tailwindcss.mjs +++ b/scripts/frontend/tailwindcss.mjs @@ -1,17 +1,11 @@ /* eslint-disable import/extensions */ -import { exec as execCB } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import util from 'node:util'; import { createProcessor } from 'tailwindcss/lib/cli/build/plugin.js'; -const exec = util.promisify(execCB); - // Note, in node > 21.2 we could replace the below with import.meta.dirname const ROOT_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'); -const cssInJsPath = path.join(ROOT_PATH, 'config/helpers/tailwind/css_in_js.js'); - export async function build({ shouldWatch = false, content = false } = {}) { const processorOptions = { '--watch': shouldWatch, @@ -47,21 +41,6 @@ function wasScriptCalledDirectly() { return process.argv[1] === fileURLToPath(import.meta.url); } -async function ensureCSSinJS() { - console.log(`Ensuring ${cssInJsPath} exists`); - const cmd = 'yarn run tailwindcss:build'; - - const { stdout, error } = await exec(cmd, { - env: { ...process.env, REDIRECT_TO_STDOUT: 'true' }, - }); - if (error) { - throw error; - } - - console.log(`'${cmd}' printed:`); - console.log(`${stdout}`); -} - export function viteTailwindCompilerPlugin({ shouldWatch = true }) { return { name: 'gitlab-tailwind-compiler', @@ -74,12 +53,17 @@ export function viteTailwindCompilerPlugin({ shouldWatch = true }) { export function webpackTailwindCompilerPlugin({ shouldWatch = true }) { return { async start() { - await ensureCSSinJS(); return build({ shouldWatch }); }, }; } if (wasScriptCalledDirectly()) { - build(); + build().then(() => { + console.log('Tailwind utils built successfully') + }).catch(e => { + console.warn('Building Tailwind utils produced an error') + console.error(e); + process.exitCode = 1; + }); } diff --git a/scripts/frontend/vite b/scripts/frontend/vite deleted file mode 100755 index 69540525acc..00000000000 --- a/scripts/frontend/vite +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# Once the tailwind migration is done, we can remove this -# and remove the `viteBinPath` entrypoint in config/vite.json -echo "Ensure that css_in_js exists to aid tailwind migration" -yarn run tailwindcss:build - -echo "Starting vite" -exec node_modules/.bin/vite "$@" diff --git a/spec/frontend/scripts/frontend/tailwind_lint_against_legacy_utils_spec.js b/spec/frontend/scripts/frontend/tailwind_lint_against_legacy_utils_spec.js new file mode 100644 index 00000000000..d95df0d153b --- /dev/null +++ b/spec/frontend/scripts/frontend/tailwind_lint_against_legacy_utils_spec.js @@ -0,0 +1,40 @@ +import { lintAgainstLegacyUtils } from '../../../../scripts/frontend/tailwind_lint_against_legacy_utils'; + +describe('lintAgainstLegacyUtils', () => { + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(jest.fn()); + jest.spyOn(console, 'warn').mockImplementation(jest.fn()); + }); + + it('does not throw if no legacy utils are used', async () => { + await expect( + lintAgainstLegacyUtils({ content: [{ raw: '
', extension: 'html' }] }), + ).resolves.not.toThrow(); + }); + + describe('legacy utils are used', () => { + it('does throw on basic legacy utils', async () => { + await expect( + lintAgainstLegacyUtils({ + content: [{ raw: '
', extension: 'html' }], + }), + ).rejects.toThrow(/You are introducing legacy utilities[\s\S]+\.gl-display-block/gm); + }); + + it('does throw with modified legacy utils', async () => { + await expect( + lintAgainstLegacyUtils({ + content: [{ raw: '
', extension: 'html' }], + }), + ).rejects.toThrow(/You are introducing legacy utilities[\s\S]+\.md\\:gl-display-block/gm); + }); + + it('does throw with important legacy utils', async () => { + await expect( + lintAgainstLegacyUtils({ + content: [{ raw: '
', extension: 'html' }], + }), + ).rejects.toThrow(/You are introducing legacy utilities[\s\S]+\.\\!gl-display-block/gm); + }); + }); +}); diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 751611be5d2..c15433f7fc9 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -82,6 +82,28 @@ RSpec.describe Gitlab::Git::Blame, feature_category: :source_code_management do end end + context 'when repository has SHA256 format' do + let_it_be(:user) { create(:user, :with_namespace) } + + let(:project) { Projects::CreateService.new(user, opts).execute } + let(:opts) do + { + name: 'SHA256', + namespace_id: user.namespace.id, + initialize_with_readme: true, + repository_object_format: 'sha256' + } + end + + let(:sha) { project.commit.sha } + let(:path) { 'README.md' } + + it 'correctly blames file' do + expect(result).to be_present + expect(result.first[:commit].sha.size).to eq(64) + end + end + context "renamed file" do let(:commit) { project.commit('blame-on-renamed') } let(:sha) { commit.id } diff --git a/yarn.lock b/yarn.lock index 7988ae4023f..555f37c1fd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12363,11 +12363,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rgb-hex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/rgb-hex/-/rgb-hex-4.1.0.tgz#b343f394c8f9df629a7504c34c00b5bc63bd006f" - integrity sha512-UZLM57BW09Yi9J1R3OP8B1yCbbDK3NT8BDtihGZkGkGEs2b6EaV85rsfJ6yK4F+8UbxFFmfA+9xHT5ZWhN1gDQ== - rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"