Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-09-06 00:13:51 +00:00
parent 94bf8a50bb
commit 11501d600a
35 changed files with 868 additions and 1948 deletions

View File

@ -0,0 +1,3 @@
import createDefaultClient from '~/lib/graphql';
export const graphqlClient = createDefaultClient();

View File

@ -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,

View File

@ -1,4 +0,0 @@
@import 'page_bundles/mixins_and_variables_and_functions';
// Gitlab UI util classes
@import '@gitlab/ui/src/scss/utilities';

View File

@ -1,3 +0,0 @@
@import './themes/dark';
@import 'application_utilities_to_be_replaced';

View File

@ -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';

View File

@ -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;

View File

@ -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 {

View File

@ -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).

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1,3 @@
@import 'mixins_and_variables_and_functions';
@import './notes/system_notes_v2';
@import './notes/note_actions';

View File

@ -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;

View File

@ -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);
}),
],
};

View File

@ -16,7 +16,6 @@
],
"port": 3038,
"publicOutputDir": "vite-dev",
"viteBinPath": "scripts/frontend/vite",
"devServerConnectTimeout": 3
}
}

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View 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]

View File

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

View File

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

View File

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

View File

@ -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');
}

View File

@ -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;
});
}

View File

@ -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;
});
}

View File

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

View File

@ -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,
};

View File

@ -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;
});
}

View File

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

View File

@ -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);
});
});
});

View File

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

View File

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