Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
94bf8a50bb
commit
11501d600a
|
|
@ -0,0 +1,3 @@
|
|||
import createDefaultClient from '~/lib/graphql';
|
||||
|
||||
export const graphqlClient = createDefaultClient();
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
@import 'page_bundles/mixins_and_variables_and_functions';
|
||||
|
||||
// Gitlab UI util classes
|
||||
@import '@gitlab/ui/src/scss/utilities';
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
@import './themes/dark';
|
||||
|
||||
@import 'application_utilities_to_be_replaced';
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 <mark> (allowed in Markdown).
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
@import './notes/system_notes_v2';
|
||||
@import './notes/note_actions';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
],
|
||||
"port": 3038,
|
||||
"publicOutputDir": "vite-dev",
|
||||
"viteBinPath": "scripts/frontend/vite",
|
||||
"devServerConnectTimeout": 3
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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<String>}
|
||||
*/
|
||||
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<String>} tailwindClassNames
|
||||
* @param {Set<String>} 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<String>} oldClassNames
|
||||
* @param {Array<string>} content
|
||||
* @returns {Promise<{rules: Set<String>}>}
|
||||
*/
|
||||
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:
|
||||
*
|
||||
* <div class="gl-display-flex">
|
||||
* <div class="gl-foo-bar">
|
||||
* @type {string}
|
||||
*/
|
||||
const allLegacyDocument = Array.from(legacyClassNames)
|
||||
.map((className) => {
|
||||
const cleanClass = className
|
||||
.substring(1)
|
||||
// replace escaped `\!` with !
|
||||
.replace(/\\!/g, '!');
|
||||
|
||||
return `<div class="${cleanClass}"></div>`;
|
||||
})
|
||||
.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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "$@"
|
||||
|
|
@ -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: '<div class="gl-block">', extension: 'html' }] }),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
describe('legacy utils are used', () => {
|
||||
it('does throw on basic legacy utils', async () => {
|
||||
await expect(
|
||||
lintAgainstLegacyUtils({
|
||||
content: [{ raw: '<div class="gl-display-block">', 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: '<div class="md:gl-display-block">', 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: '<div class="!gl-display-block">', extension: 'html' }],
|
||||
}),
|
||||
).rejects.toThrow(/You are introducing legacy utilities[\s\S]+\.\\!gl-display-block/gm);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue