Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(RedwoodUI) Add setup command for RedwoodUI #11596

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e8ab4d5
Start adding redwoodui setup command
arimendelow Sep 18, 2024
266195e
More to do in terms of getting the output of sub-processes to show up…
arimendelow Sep 18, 2024
a89bab9
Starts to modify the tailwind config!
arimendelow Sep 18, 2024
2230a32
Having backups is too redundant for my tastes
arimendelow Sep 18, 2024
de5e07b
Adding colors to tw config is a go!
arimendelow Sep 19, 2024
0cec2c5
Move config modifying utils to new files as they're hella long
arimendelow Sep 19, 2024
483843d
Add comment
arimendelow Sep 19, 2024
8fec6fa
Add plugins config
arimendelow Sep 19, 2024
9c2145c
Start to handle css
arimendelow Sep 19, 2024
6100353
Start to switch to better structure by using sub tasks
arimendelow Sep 19, 2024
803b77f
More work on making this print out all nice
arimendelow Sep 19, 2024
d63650e
Plugins are a go!
arimendelow Sep 19, 2024
7395896
First pass at adding base layer classes is a go!
arimendelow Sep 20, 2024
b23d669
Better address conflicting classes and such
arimendelow Sep 20, 2024
55549e3
Generalize add layer util — it's not perfect yet though
arimendelow Sep 20, 2024
9da80ac
Fix some newlines
arimendelow Sep 20, 2024
35f4871
Ah, it was incorrectly adding things because we were removing from th…
arimendelow Sep 20, 2024
06ce39e
Write out the remaining tasks, and some small cleanup
arimendelow Sep 20, 2024
3a696cf
Need to figure out correct path alias but this does at least work
arimendelow Sep 20, 2024
f2dc814
Fix json parsing error handling
arimendelow Sep 21, 2024
a522d09
More work in terms of better skip logging, and deps install is a go
arimendelow Sep 21, 2024
0b50561
Move storybook related items into sub tasks
arimendelow Sep 21, 2024
8b4c3a2
Add not-impl warning
arimendelow Sep 21, 2024
e43c2eb
Add uiutils
arimendelow Sep 24, 2024
e0a6f1c
Handle when getting dir from git
arimendelow Sep 24, 2024
5470c28
Got the list of components to add!
arimendelow Sep 24, 2024
156d7a3
Fix some type problems
arimendelow Sep 25, 2024
7071ba6
Add stub function for addFileAndInstallPackages
arimendelow Sep 25, 2024
be28e04
Some more work towards installing components and setting up sb
arimendelow Sep 25, 2024
4bdb6ac
Remove 'force' as we weren't listening to it in many places anyway. A…
arimendelow Sep 25, 2024
5f96e63
Adding components is a go!
arimendelow Sep 26, 2024
5eb67f4
Remove changes to tsconfig — decided that adding another path alias w…
arimendelow Sep 26, 2024
38640b7
Continue converting to class, start to add helpers for installing com…
arimendelow Sep 30, 2024
7b8fc0d
Finish first step of converting to class
arimendelow Sep 30, 2024
8b0446e
Cleanup and first pass at parity with pre-class, plus first pass at a…
arimendelow Sep 30, 2024
6ce0d94
Forgot to await — installing components is a go!
arimendelow Sep 30, 2024
6bc3110
Improved install logging and add todo
arimendelow Sep 30, 2024
d2bcb97
Cleanup and add shared form things
arimendelow Oct 1, 2024
e85d35e
start working on setting up sb
arimendelow Oct 2, 2024
6dfe61e
Move shared utils to new file and add function for adding import stat…
arimendelow Oct 2, 2024
911592c
Finish first pass at sb setup
arimendelow Oct 2, 2024
35e8bb4
Add success message
arimendelow Oct 2, 2024
f4eee84
More work on SB config setup, and start to address more cases
arimendelow Oct 2, 2024
3a03aad
Clean up output logging to use a helper function, and storybook confi…
arimendelow Oct 4, 2024
63c1de9
Remove extra debugging logging
arimendelow Oct 4, 2024
cf20ab7
Handle Unauthorized res from github
arimendelow Oct 4, 2024
c784247
Further work on making SB setup work
arimendelow Oct 4, 2024
e7c3219
Was missing some awaits. Also, add checking of root package.json when…
arimendelow Oct 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import type { ListrTaskWrapper } from 'listr2'

import c from '../../../../../lib/colors'

import { logTaskOutput } from './sharedUtils'

/**
* Adds the RedwoodUI colors TailwindCSS configuration to the project's TailwindCSS configuration.
* - If the project doesn't have a colors config, it will add it.
* - If the project does have a colors config, it will check that all required colors are set.
* - If any required colors are missing, it will add them.
*
* Rather than writing the new config to the file, it will return the new config as a string.
* This is so that we can iteratively build up the new config and then write it to the file at the end.
*
* Regardless of whether it was modified, it will return the new config so that multiple
* of these transformations can be easily chained together.
*/
const addColorsConfigToProjectTailwindConfig = (
task: ListrTaskWrapper<any, any>,
rwuiColorsConfig: string,
projectColorsConfig: string | null,
projectTailwindConfig: string,
): string => {
let needToAddTWColorsImport = false
let configToReturn = projectTailwindConfig

// Regular expression to match all key-value pairs
const regex = /(\w+):/g

// If there is no project colors config to begin with, add the entire RWUI colors config
if (!projectColorsConfig) {
// Add the rwuiColorsConfig to the projectTailwindConfig under theme.extend.colors
// First, check if theme.extend exists in the project config
const themeExtendMatch = projectTailwindConfig.match(
/theme:\s*{\s*extend:\s*({[^}]*})/s,
)
const themeExtendConfig = themeExtendMatch
? themeExtendMatch[1].trim()
: null

if (themeExtendConfig) {
// If theme.extend exists, add the colors to it
task.output = c.info(
'`theme.extend` exists in your TailwindCSS config. Adding the required colors to it...',
)
configToReturn = projectTailwindConfig.replace(
/theme:\s*{\s*extend:\s*{[^}]*}/s,
(match) => {
return match.replace(
/extend:\s*{/,
`extend: {\n colors: ${rwuiColorsConfig},\n `,
)
},
)
} else {
// If theme.extend does not exist, add theme.extend with colors
task.output = c.info(
'`theme.extend` does not exist in your TailwindCSS config. Adding it with the required colors...',
)
configToReturn = projectTailwindConfig.replace(
/module.exports = {/,
`module.exports = {\n theme: {\n extend: {\n colors: ${rwuiColorsConfig},\n },\n },`,
)
}

logTaskOutput(
task,
c.success(
`\nAdded RedwoodUI's colors configuration to your project's TailwindCSS configuration.\nPlease confirm that the config has been added correctly by checking your TailwindCSS config file.`,
),
)

needToAddTWColorsImport = true
} else {
// Here, we know that the project has *some* colors config, but we don't know if it's correct.
// For example, it's totally possible that a user has added their own colors config to their project.

// Get all the keys currently in the project's colors config, and all the keys that are required
// This shouldn't match anything that's in a comment
const projectKeys = Array.from(
new Set(
projectColorsConfig
?.split('\n')
?.filter(
(line) =>
!line.trim().startsWith('//') && !line.trim().startsWith('*'),
)
?.join('\n')
?.match(regex)
?.map((match) => match.replace(':', '').trim()) || [],
),
)
const requiredKeys = Array.from(
new Set(
rwuiColorsConfig
.split('\n')
?.filter(
(line) =>
!line.trim().startsWith('//') && !line.trim().startsWith('*'),
)
?.join('\n')
?.match(regex)
?.map((match) => match.replace(':', '').trim()) || [],
),
)

// Check if the project colors config has all the required keys
const missingKeys = requiredKeys.filter((key) => !projectKeys.includes(key))

if (missingKeys.length === 0) {
task.skip(
"Your project's TailwindCSS configuration already includes all required colors.",
)
return projectTailwindConfig
} else if (missingKeys.length === requiredKeys.length) {
// If all keys are missing, add the entire RWUI colors config to the bottom of the project colors config
const rwuiColorsConfigWithoutBraces = rwuiColorsConfig
.replace(/^{|}$/g, '')
.trim()

configToReturn = projectTailwindConfig.replace(
/colors:\s*{[^}]*}/s,
(match) => {
return match.replace(/{[^}]*}/s, (innerMatch) => {
return innerMatch.replace(
/}$/,
` ${rwuiColorsConfigWithoutBraces}\n }`,
)
})
},
)
logTaskOutput(
task,
c.success(
`\nLooks like you already had some custom colors config — added RedwoodUI's colors configuration to your project's TailwindCSS configuration.\nPlease confirm that the config has been added correctly by checking your TailwindCSS config file.`,
),
)

needToAddTWColorsImport = true
} else {
// If there are only some missing keys, warn the user to consult the RedwoodUI
// config and add the missing keys to their project's config
task.output = c.warning(
`Your project's TailwindCSS configuration is missing some required colors.\nIf this happened, it's likely you're already using some of the color names required by RedwoodUI, so we haven't overwritten them.\nPlease check your colors configuration and ensure it also includes the following keys:\n${missingKeys.join(', ')}`,
)
throw new Error(
"Ran into a conflict setting the project's TailwindCSS colors configuration.",
)
}
}

if (needToAddTWColorsImport) {
// Rather than extracting this from the RWUI colors config, we'll just hardcode it
// because it's unlikely to change unless we fundamentally change how we do the default theme.
const colorsImport = "const colors = require('tailwindcss/colors')\n\n"
// Check if the project has the colors import
if (!configToReturn.includes(colorsImport)) {
// Add the colors import to the top of the file
configToReturn = colorsImport + configToReturn
logTaskOutput(
task,
c.success(
'\nAdded TailwindCSS color pallette import (used by TWUI default colors) to your config.',
),
)
} else {
task.skip(
'Your TailwindCSS config already includes the TailwindCSS color pallette import (used by TWUI default colors), so we did not add it again.',
)
}
}

return configToReturn
}

export default addColorsConfigToProjectTailwindConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { ListrTaskWrapper } from 'listr2'

import c from '../../../../../lib/colors'

/**
* Adds the RedwoodUI darkMode TailwindCSS configuration to the project's TailwindCSS configuration.
* - If the project doesn't have a darkMode config, it will add it.
* - If the project does have a darkMode config, it will check if it matches the RedwoodUI darkMode config.
* - If it doesn't match, it will print a warning that the user should check their darkMode config.
*
* Rather than writing the new config to the file, it will return the new config as a string.
* This is so that we can iteratively build up the new config and then write it to the file at the end.
*
* Will return the new config if it succeeds, and throw an error if it fails.
*/
const addDarkModeConfigToProjectTailwindConfig = (
task: ListrTaskWrapper<any, any>,
rwuiDarkModeConfig: string,
projectDarkModeConfig: string | null,
projectTailwindConfig: string,
): string => {
// if the project doesn't have a darkMode config, add it
if (!projectDarkModeConfig) {
// add the rwuiDarkModeConfig to the projectTailwindConfig
const newConfig = projectTailwindConfig.replace(
/module.exports = {/,
`module.exports = {\n darkMode: ${rwuiDarkModeConfig},`,
)
task.output = c.success(
"Added RedwoodUI's darkMode configuration to your project's TailwindCSS configuration.",
)

return newConfig
} else {
// if the project does have a darkMode config, check if it matches the rwuiDarkModeConfig
// if it doesn't match, print a warning that the user should check their darkMode config
// and possibly update it to match the rwuiDarkModeConfig
if (projectDarkModeConfig !== rwuiDarkModeConfig) {
task.output = c.warning(
`Warning: Your project's TailwindCSS configuration already has a darkMode config that is different from RedwoodUI's, and may not work.\nPlease check your darkMode setting and ensure it matches RedwoodUI's.\n\nRedwoodUI darkMode setting: ${rwuiDarkModeConfig}\nYour project's darkMode setting: ${projectDarkModeConfig}\n\nMore info here: https://tailwindcss.com/docs/dark-mode#customizing-the-selector`,
)
throw new Error(
"Ran into a conflict setting the project's TailwindCSS darkMode configuration.",
)
} else {
task.skip(
"Your project's TailwindCSS configuration already has the correct darkMode setting.",
)
return projectTailwindConfig
}
}
}

export default addDarkModeConfigToProjectTailwindConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type { ListrTaskWrapper } from 'listr2'

import c from '../../../../../lib/colors'

import { logTaskOutput } from './sharedUtils'

const addLayerToIndexCSS = (
task: ListrTaskWrapper<any, any>,
layerName: 'base' | 'components',
rwuiLayerContentToAdd: string,
projectLayerContent: string | null,
projectIndexCSS: string,
): string => {
let newCSSContent = projectIndexCSS
if (!projectLayerContent) {
// If the project doesn't have the layer, check if there's an empty one or none at all.
// Doesn't include if commented out.
const hasEmptyLayer = projectIndexCSS
.split('\n')
.some(
(line) =>
line.includes(`@layer ${layerName} {`) &&
!line.trim().startsWith('//') &&
!line.trim().startsWith('*'),
)

// If there's an empty layer, replace it with the RWUI layer.
if (hasEmptyLayer) {
newCSSContent = projectIndexCSS.replace(
new RegExp(`@layer ${layerName} {[^}]*}`, 's'),
`@layer ${layerName} {\n ${rwuiLayerContentToAdd}\n}`,
)
task.output = c.success(
`Added RedwoodUI's ${layerName} layer to your project's index.css.`,
)
} else {
// If there's no base layer, add the RWUI base layer to the end of the file.
newCSSContent =
projectIndexCSS +
`\n@layer ${layerName} {\n ${rwuiLayerContentToAdd}\n}`
task.output = c.success(
`Added RedwoodUI's ${layerName} layer to your project's index.css.`,
)
}
} else {
// If the project does have the layer, check whether its classes have the same name as those of the RWUI layer.
// Note that in the base layer, these are HTML tags, not classes. Therefore, they won't start with a dot.
// In the components layer, they will start with a dot, and can also be concatenated with other classes (e.g. `.rw-button .primary`).
const classPattern =
layerName === 'base'
? /[a-zA-Z0-9_-]+(?=\s*\{)/g
: /\.([a-zA-Z0-9_-]+(?:\s+\.[a-zA-Z0-9_-]+)*)(?=\s*\{)/g

// Creating two lists here: one to iterate over, and one that we can safely
// remove items from without affecting the iteration.
// If we don't do this, it'll skip items when doing the .forEach loop.
const potentialClassesToAdd: string[] =
rwuiLayerContentToAdd.match(classPattern) || []
const classesToAdd = [...potentialClassesToAdd]

// For each class that we want to add, check if it already exists in the project's layer.
// If it does, check if it's the same as the RWUI layer class.
// If it is, remove it from the list of classes to add.
// If it's not, add it to a list of conflicting classes.
const conflictingClasses: string[] = []

potentialClassesToAdd.forEach((className) => {
const classRegex = new RegExp(`(${className}\\s*{[^}]*})`, 's')
const rwuiClassMatch = rwuiLayerContentToAdd.match(classRegex)
const projectClassMatch = projectLayerContent.match(classRegex)

if (projectClassMatch) {
// If the class exists in the project's layer, check if it's the same as the RWUI layer class.
// TODO: This is a naive check. It doesn't account for whitespace or ordering differences.
if (rwuiClassMatch && rwuiClassMatch[0] === projectClassMatch[0]) {
// If it is the same, just remove it from the list of classes to add.
classesToAdd.splice(classesToAdd.indexOf(className), 1)
} else {
// If it's not the same, add it to the list of conflicting classes and remove it from the list of classes to add.
conflictingClasses.push(className)
classesToAdd.splice(classesToAdd.indexOf(className), 1)
}
}
})

// Now, if there's no conflicting classes or classes to add, we don't need to do anything more.
if (conflictingClasses.length === 0 && classesToAdd.length === 0) {
task.skip(
`Your project's ${layerName} layer already has the correct classes.`,
)
return projectIndexCSS
} else if (classesToAdd.length > 0) {
// If there are classes to add, add them.
// Remember that right now we just have a list of class names, not the full class definition,
// so we need to create the list of class definitions to add.
const classesToAddString = classesToAdd
.map((className) => {
const classRegex = new RegExp(`(${className}\\s*{[^}]*})`, 's')
const rwuiClassMatch = rwuiLayerContentToAdd.match(classRegex)
return rwuiClassMatch ? rwuiClassMatch[0] : ''
})
.join('\n ')

newCSSContent = addToEndOfLayer(
layerName,
classesToAddString,
projectIndexCSS,
)
task.output = c.success(
`Added the following missing classes to your project's ${layerName} layer in index.css:\n` +
`${classesToAdd.join(', ')}`,
)
if (conflictingClasses.length > 0) {
logTaskOutput(
task,
c.warning(
`\nSome classes in RedwoodUI's ${layerName} layer were not added to your project's ${layerName} layer because they conflict with existing classes.\nPlease review the following classes in the ${layerName} layer of your index.css:\n` +
`${conflictingClasses.join(', ')}`,
),
)
}
} else {
// If there are no classes to add, but there are conflicting classes, throw an error.
throw new Error(
`Added no classes to your project's ${layerName} layer, because they all conflicted with your existing classes.\nPlease review the following classes in the ${layerName} layer of your index.css:\n` +
`${conflictingClasses.join(', ')}`,
)
}
}

return newCSSContent
}

export default addLayerToIndexCSS

const addToEndOfLayer = (
layerName: string,
layerContent: string,
cssFileContent: string,
): string => {
return cssFileContent.replace(
new RegExp(`(@layer ${layerName}\\s*{)([^{}]*(?:{[^{}]*}[^{}]*)*)}`, 's'),
`$1$2 ${layerContent}\n}`,
)
}
Loading
Loading