From 17712ca4134b4575f8b7c7feae06a5614e40bfb1 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 15 Jan 2024 18:08:28 +0100 Subject: [PATCH] Add cli-helpers util to update redwood.toml (#9832) --- .../src/lib/__tests__/project.test.ts | 9 +- packages/cli-helpers/src/lib/project.ts | 138 +++++++++++++++++- .../__tests__/fragmentsHandler.test.ts | 10 -- .../features/fragments/fragmentsHandler.ts | 68 +-------- .../trustedDocumentsHandler.ts | 90 ++---------- 5 files changed, 161 insertions(+), 154 deletions(-) diff --git a/packages/cli-helpers/src/lib/__tests__/project.test.ts b/packages/cli-helpers/src/lib/__tests__/project.test.ts index 3aef810bc9bc..a8aa58145848 100644 --- a/packages/cli-helpers/src/lib/__tests__/project.test.ts +++ b/packages/cli-helpers/src/lib/__tests__/project.test.ts @@ -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: { diff --git a/packages/cli-helpers/src/lib/project.ts b/packages/cli-helpers/src/lib/project.ts index 7755671f2aad..f1cd4d2b7225 100644 --- a/packages/cli-helpers/src/lib/project.ts +++ b/packages/cli-helpers/src/lib/project.ts @@ -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, @@ -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) +} diff --git a/packages/cli/src/commands/setup/graphql/features/fragments/__tests__/fragmentsHandler.test.ts b/packages/cli/src/commands/setup/graphql/features/fragments/__tests__/fragmentsHandler.test.ts index 2e271612b4a4..da1ec1b0b94a 100644 --- a/packages/cli/src/commands/setup/graphql/features/fragments/__tests__/fragmentsHandler.test.ts +++ b/packages/cli/src/commands/setup/graphql/features/fragments/__tests__/fragmentsHandler.test.ts @@ -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) diff --git a/packages/cli/src/commands/setup/graphql/features/fragments/fragmentsHandler.ts b/packages/cli/src/commands/setup/graphql/features/fragments/fragmentsHandler.ts index 79ed26194b76..b9bb28c41b50 100644 --- a/packages/cli/src/commands/setup/graphql/features/fragments/fragmentsHandler.ts +++ b/packages/cli/src/commands/setup/graphql/features/fragments/fragmentsHandler.ts @@ -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' @@ -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( [ { @@ -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) }, }, { diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts index fe93c5d6fc22..07c777e2c22e 100644 --- a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts @@ -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) }, }, {