Skip to content

Commit

Permalink
Add cli-helpers util to update redwood.toml (redwoodjs#9832)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Jan 15, 2024
1 parent 3f9a902 commit 17712ca
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 154 deletions.
9 changes: 5 additions & 4 deletions packages/cli-helpers/src/lib/__tests__/project.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import fs from 'fs'
jest.mock('fs')
jest.mock('node:fs')

import toml from '@iarna/toml'
import * as fs from 'node:fs'

import { updateTomlConfig, addEnvVar } from '../project' // Replace with the correct path to your module
import * as toml from '@iarna/toml'

jest.mock('fs')
import { updateTomlConfig, addEnvVar } from '../project'

const defaultRedwoodToml = {
web: {
Expand Down
138 changes: 136 additions & 2 deletions packages/cli-helpers/src/lib/project.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import fs from 'fs'
import path from 'path'
import * as fs from 'node:fs'
import * as path from 'node:path'

import type { JsonMap } from '@iarna/toml'
import toml from '@iarna/toml'
import dotenv from 'dotenv'

import type { Config } from '@redwoodjs/project-config'
import {
findUp,
getConfigPath,
Expand Down Expand Up @@ -189,3 +190,136 @@ export const setRedwoodCWD = (cwd?: string) => {

process.env.RWJS_CWD = cwd
}

/**
* Create or update the given setting, in the given section, with the given value.
*
* If the section already exists it adds the new setting last
* If the section, and the setting, already exists, the setting is updated
* If the section does not exist it is created at the end of the file and the setting is added
* If the setting exists in the section, but is commented out, it will be uncommented and updated
*/
export function setTomlSetting(
section: keyof Config,
setting: string,
value: string | boolean | number
) {
const redwoodTomlPath = getConfigPath()
const originalTomlContent = fs.readFileSync(redwoodTomlPath, 'utf-8')

// Can't type toml.parse because this PR has not been included in a released yet
// https://github.com/iarna/iarna-toml/commit/5a89e6e65281e4544e23d3dbaf9e8428ed8140e9
const redwoodTomlObject = toml.parse(originalTomlContent) as any

const existingValue = redwoodTomlObject?.[section]?.[setting]

// If the setting already exists in the given section, and has the given
// value already, just return
if (existingValue === value) {
return
}

// By default we create the new section at the end of the file, and set the
// new value for the given setting. If the section already exists, we'll
// disregard this update and use the existing section instead
let newTomlContent =
originalTomlContent.replace(/\n$/, '') +
`\n\n[${section}]\n ${setting} = ${value}`

const hasExistingSettingSection = !!redwoodTomlObject?.[section]

if (hasExistingSettingSection) {
const existingSectionSettings = Object.keys(redwoodTomlObject[section])

let inSection = false
let indentation = ''
let insertionIndex = 1
let updateExistingValue = false
let updateExistingCommentedValue = false

const tomlLines = originalTomlContent.split('\n')

// Loop over all lines looking for either the given setting in the given
// section (preferred), or the given setting, but commented out, in the
// given section
tomlLines.forEach((line: string, index) => {
// Assume all sections start with [sectionName] un-indented. This might
// prove to be too simplistic, but it's all we support right now. Feel
// free to add support for more complicated scenarios as needed.
if (line.startsWith(`[${section}]`)) {
inSection = true
insertionIndex = index + 1
} else {
// The section ends as soon as we find a line that starts with a [
if (/^\s*\[/.test(line)) {
inSection = false
}

// If we're in the section, and we haven't found the setting yet, keep
// looking
if (inSection && !updateExistingValue) {
for (const existingSectionSetting of existingSectionSettings) {
const matches = line.match(
new RegExp(`^(\\s*)${existingSectionSetting}\\s*=`, 'i')
)

if (!updateExistingValue && matches) {
if (!updateExistingCommentedValue) {
indentation = matches[1]
}

if (existingSectionSetting === setting) {
updateExistingValue = true
insertionIndex = index
indentation = matches[1]
}
}

// As long as we find existing settings in the section we keep
// pushing the insertion index forward, unless we've already found
// an existing setting that matches the one we're adding.
if (
!updateExistingValue &&
!updateExistingCommentedValue &&
/^\s*\w+\s*=/.test(line)
) {
insertionIndex = index + 1
}
}

// If we haven't found an existing value to update, see if we can
// find a commented value instead
if (!updateExistingValue) {
const matchesComment = line.match(
new RegExp(`^(\\s*)#(\\s*)${setting}\\s*=`, 'i')
)

if (matchesComment) {
const commentIndentation =
matchesComment[1].length > matchesComment[2].length
? matchesComment[1]
: matchesComment[2]

if (commentIndentation.length - 1 > indentation.length) {
indentation = commentIndentation
}

updateExistingCommentedValue = true
insertionIndex = index
}
}
}
}
})

tomlLines.splice(
insertionIndex,
updateExistingValue || updateExistingCommentedValue ? 1 : 0,
`${indentation}${setting} = ${value}`
)

newTomlContent = tomlLines.join('\n')
}

fs.writeFileSync(redwoodTomlPath, newTomlContent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,6 @@ afterAll(() => {
jest.resetModules()
})

test('`fragments = true` is added to redwood.toml', async () => {
vol.fromJSON({ 'redwood.toml': '', 'web/src/App.tsx': '' }, FIXTURE_PATH)

await handler({ force: false })

expect(vol.toJSON()[FIXTURE_PATH + '/redwood.toml']).toMatch(
/fragments = true/
)
})

test('all tasks are being called', async () => {
vol.fromJSON({ 'redwood.toml': '', 'web/src/App.tsx': '' }, FIXTURE_PATH)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import fs from 'node:fs'
import path from 'node:path'

import toml from '@iarna/toml'
import execa from 'execa'
import { Listr } from 'listr2'
import { format } from 'prettier'

import { colors, prettierOptions } from '@redwoodjs/cli-helpers'
import { getConfigPath, getPaths } from '@redwoodjs/project-config'
import { colors, prettierOptions, setTomlSetting } from '@redwoodjs/cli-helpers'
import { getConfig, getPaths } from '@redwoodjs/project-config'

import type { Args } from './fragments'
import { runTransform } from './runTransform'
Expand All @@ -16,12 +15,6 @@ export const command = 'fragments'
export const description = 'Set up Fragments for GraphQL'

export async function handler({ force }: Args) {
const redwoodTomlPath = getConfigPath()
const redwoodTomlContent = fs.readFileSync(redwoodTomlPath, 'utf-8')
// Can't type toml.parse because this PR has not been included in a released yet
// https://github.com/iarna/iarna-toml/commit/5a89e6e65281e4544e23d3dbaf9e8428ed8140e9
const redwoodTomlObject = toml.parse(redwoodTomlContent) as any

const tasks = new Listr(
[
{
Expand All @@ -33,66 +26,15 @@ export async function handler({ force }: Args) {
return false
}

if (redwoodTomlObject?.graphql?.fragments) {
const config = getConfig()
if (config.graphql.fragments) {
return 'GraphQL Fragments are already enabled.'
}

return false
},
task: () => {
const redwoodTomlPath = getConfigPath()
const originalTomlContent = fs.readFileSync(redwoodTomlPath, 'utf-8')
const hasExistingGraphqlSection = !!redwoodTomlObject?.graphql

let newTomlContent =
originalTomlContent.replace(/\n$/, '') +
'\n\n[graphql]\n fragments = true'

if (hasExistingGraphqlSection) {
const existingGraphqlSetting = Object.keys(
redwoodTomlObject.graphql
)

let inGraphqlSection = false
let indentation = ''
let lastGraphqlSettingIndex = 0

const tomlLines = originalTomlContent.split('\n')
tomlLines.forEach((line, index) => {
if (line.startsWith('[graphql]')) {
inGraphqlSection = true
lastGraphqlSettingIndex = index
} else {
if (/^\s*\[/.test(line)) {
inGraphqlSection = false
}
}

if (inGraphqlSection) {
const matches = line.match(
new RegExp(`^(\\s*)(${existingGraphqlSetting})\\s*=`, 'i')
)

if (matches) {
indentation = matches[1]
}

if (/^\s*\w+\s*=/.test(line)) {
lastGraphqlSettingIndex = index
}
}
})

tomlLines.splice(
lastGraphqlSettingIndex + 1,
0,
`${indentation}fragments = true`
)

newTomlContent = tomlLines.join('\n')
}

fs.writeFileSync(redwoodTomlPath, newTomlContent)
setTomlSetting('graphql', 'fragments', true)
},
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,96 +1,36 @@
import fs from 'node:fs'
import path from 'node:path'

import toml from '@iarna/toml'
import execa from 'execa'
import { Listr } from 'listr2'
import { format } from 'prettier'

import { prettierOptions } from '@redwoodjs/cli-helpers'
import { getConfigPath, getPaths, resolveFile } from '@redwoodjs/project-config'
import { prettierOptions, setTomlSetting } from '@redwoodjs/cli-helpers'
import { getConfig, getPaths, resolveFile } from '@redwoodjs/project-config'

import { runTransform } from '../fragments/runTransform'

function updateRedwoodToml(redwoodTomlPath: string) {
const originalTomlContent = fs.readFileSync(redwoodTomlPath, 'utf-8')
const redwoodTomlContent = fs.readFileSync(redwoodTomlPath, 'utf-8')
// Can't type toml.parse because this PR has not been included in a released yet
// https://github.com/iarna/iarna-toml/commit/5a89e6e65281e4544e23d3dbaf9e8428ed8140e9
const redwoodTomlObject = toml.parse(redwoodTomlContent) as any
const hasExistingGraphqlSection = !!redwoodTomlObject?.graphql

if (redwoodTomlObject?.graphql?.trustedDocuments) {
console.info(
'GraphQL Trusted Documents are already enabled in your Redwood project.'
)

return { newConfig: undefined, trustedDocumentsExists: true }
}

let newTomlContent =
originalTomlContent.replace(/\n$/, '') +
'\n\n[graphql]\n trustedDocuments = true'

if (hasExistingGraphqlSection) {
const existingGraphqlSetting = Object.keys(redwoodTomlObject.graphql)

let inGraphqlSection = false
let indentation = ''
let lastGraphqlSettingIndex = 0

const tomlLines = originalTomlContent.split('\n')
tomlLines.forEach((line, index) => {
if (line.startsWith('[graphql]')) {
inGraphqlSection = true
lastGraphqlSettingIndex = index
} else {
if (/^\s*\[/.test(line)) {
inGraphqlSection = false
}
}

if (inGraphqlSection) {
const matches = line.match(
new RegExp(`^(\\s*)(${existingGraphqlSetting})\\s*=`, 'i')
)

if (matches) {
indentation = matches[1]
}

if (/^\s*\w+\s*=/.test(line)) {
lastGraphqlSettingIndex = index
}
}
})

tomlLines.splice(
lastGraphqlSettingIndex + 1,
0,
`${indentation}trustedDocuments = true`
)

newTomlContent = tomlLines.join('\n')
}

return { newConfig: newTomlContent, trustedDocumentsExists: false }
}

export async function handler({ force }: { force: boolean }) {
const tasks = new Listr(
[
{
title:
'Update Redwood Project Configuration to enable GraphQL Trusted Documents ...',
task: () => {
const redwoodTomlPath = getConfigPath()

const { newConfig, trustedDocumentsExists } =
updateRedwoodToml(redwoodTomlPath)
skip: () => {
if (force) {
// Never skip when --force is used
return false
}

if (newConfig && (force || !trustedDocumentsExists)) {
fs.writeFileSync(redwoodTomlPath, newConfig, 'utf-8')
const config = getConfig()
if (config.graphql.trustedDocuments) {
return 'GraphQL Trusted Documents are already enabled in your Redwood project.'
}

return false
},
task: () => {
setTomlSetting('graphql', 'trustedDocuments', true)
},
},
{
Expand Down

0 comments on commit 17712ca

Please sign in to comment.