diff --git a/.changeset/brave-tigers-dance.md b/.changeset/brave-tigers-dance.md new file mode 100644 index 000000000..00830b1b3 --- /dev/null +++ b/.changeset/brave-tigers-dance.md @@ -0,0 +1,5 @@ +--- +'@graphql-hive/cli': patch +--- + +handle escaped single-quoted strings in schema changes diff --git a/integration-tests/fixtures/enum-with-deprecation-change.graphql b/integration-tests/fixtures/enum-with-deprecation-change.graphql new file mode 100644 index 000000000..0c39225b2 --- /dev/null +++ b/integration-tests/fixtures/enum-with-deprecation-change.graphql @@ -0,0 +1,10 @@ +type Query { + status: Status +} + +enum Status { + ACTIVE + INACTIVE @deprecated(reason: "Use 'DISABLED' instead, it's clearer") + PENDING + DISABLED +} diff --git a/integration-tests/fixtures/enum-with-deprecation-init.graphql b/integration-tests/fixtures/enum-with-deprecation-init.graphql new file mode 100644 index 000000000..95c27462b --- /dev/null +++ b/integration-tests/fixtures/enum-with-deprecation-init.graphql @@ -0,0 +1,9 @@ +type Query { + status: Status +} + +enum Status { + ACTIVE + INACTIVE + PENDING +} diff --git a/integration-tests/tests/cli/schema.spec.ts b/integration-tests/tests/cli/schema.spec.ts index 5cbefb2d9..1de76d678 100644 --- a/integration-tests/tests/cli/schema.spec.ts +++ b/integration-tests/tests/cli/schema.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable no-process-env */ import { createHash } from 'node:crypto'; +import stripAnsi from 'strip-ansi'; import { ProjectType, RuleInstanceSeverityLevel } from 'testkit/gql/graphql'; import * as GraphQLSchema from 'testkit/gql/graphql'; import type { CompositeSchema } from '@hive/api/__generated__/types'; @@ -974,3 +975,33 @@ test.concurrent( ).rejects.toThrow('Failed to auto-approve: Schema check has schema policy errors'); }, ); + +test.concurrent( + 'schema:check displays enum deprecation reason with single quotes correctly', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject } = await createOrg(); + const { createTargetAccessToken } = await createProject(ProjectType.Single); + const { secret } = await createTargetAccessToken({}); + + await schemaPublish([ + '--registry.accessToken', + secret, + '--author', + 'Test', + '--commit', + 'init', + 'fixtures/enum-with-deprecation-init.graphql', + ]); + + const result = await schemaCheck([ + '--registry.accessToken', + secret, + 'fixtures/enum-with-deprecation-change.graphql', + ]); + + expect(stripAnsi(result)).toContain( + "Enum value Status.INACTIVE was deprecated with reason Use 'DISABLED' instead, it's clearer", + ); + }, +); diff --git a/integration-tests/tests/cli/texture.spec.ts b/integration-tests/tests/cli/texture.spec.ts new file mode 100644 index 000000000..33503eb31 --- /dev/null +++ b/integration-tests/tests/cli/texture.spec.ts @@ -0,0 +1,46 @@ +import colors from 'colors'; +import { boldQuotedWords } from '../../../packages/libraries/cli/src/helpers/texture/texture'; + +describe('boldQuotedWords', () => { + test('handles simple single-quoted strings', () => { + const input = "Changed value for 'foo'"; + const expected = `Changed value for ${colors.bold('foo')}`; + expect(boldQuotedWords(input)).toBe(expected); + }); + + test('handles simple double-quoted strings', () => { + const input = 'Changed value for "foo"'; + const expected = `Changed value for ${colors.bold('foo')}`; + expect(boldQuotedWords(input)).toBe(expected); + }); + + test('handles multiple quoted strings', () => { + const input = "Field 'name' on type 'User' was changed"; + const expected = `Field ${colors.bold('name')} on type ${colors.bold('User')} was changed`; + expect(boldQuotedWords(input)).toBe(expected); + }); + + test('handles string with no quotes', () => { + const input = 'No quotes here'; + expect(boldQuotedWords(input)).toBe(input); + }); + + test('handles escaped single quotes within single-quoted strings', () => { + const input = + "Enum value 'Status.INACTIVE' has deprecation reason 'Use \\'DISABLED\\' instead'"; + const expected = `Enum value ${colors.bold('Status.INACTIVE')} has deprecation reason ${colors.bold("Use 'DISABLED' instead")}`; + expect(boldQuotedWords(input)).toBe(expected); + }); + + test('handles escaped double quotes within double-quoted strings', () => { + const input = 'Default value changed from "\\"test\\"" to "other"'; + const expected = `Default value changed from ${colors.bold('"test"')} to ${colors.bold('other')}`; + expect(boldQuotedWords(input)).toBe(expected); + }); + + test('handles apostrophes when quotes are escaped', () => { + const input = "Reason 'Use \\'DISABLED\\' instead, it\\'s clearer'"; + const expected = `Reason ${colors.bold("Use 'DISABLED' instead, it's clearer")}`; + expect(boldQuotedWords(input)).toBe(expected); + }); +}); diff --git a/packages/libraries/cli/src/helpers/texture/texture.ts b/packages/libraries/cli/src/helpers/texture/texture.ts index fd3552fa7..21c7d6050 100644 --- a/packages/libraries/cli/src/helpers/texture/texture.ts +++ b/packages/libraries/cli/src/helpers/texture/texture.ts @@ -21,11 +21,15 @@ export const trimEnd = (value: string) => value.replace(/\s+$/g, ''); * Convert quoted text to bolded text. Quotes are stripped. */ export const boldQuotedWords = (value: string) => { - const singleQuotedTextRegex = /'([^']+)'/gim; - const doubleQuotedTextRegex = /"([^"]+)"/gim; + const singleQuotedTextRegex = /'((?:[^'\\]|\\.)+?)'/g; + const doubleQuotedTextRegex = /"((?:[^"\\]|\\.)+?)"/g; return value - .replace(singleQuotedTextRegex, (_, capturedValue: string) => colors.bold(capturedValue)) - .replace(doubleQuotedTextRegex, (_, capturedValue: string) => colors.bold(capturedValue)); + .replace(singleQuotedTextRegex, (_, capturedValue: string) => + colors.bold(capturedValue.replace(/\\'/g, "'")), + ) + .replace(doubleQuotedTextRegex, (_, capturedValue: string) => + colors.bold(capturedValue.replace(/\\"/g, '"')), + ); }; export const prefixedInspect =