Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# additive to .gitignore
/website/**/*.mdx
/website/next-env.d.ts
/website/pages/api-v16/**
/website/pages/api-v17/**
4 changes: 4 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,4 +1296,8 @@ export default defineConfig(
'import/unambiguous': 'off',
},
},
{
files: ['**/*.mdx'],
processor: 'internal-rules/mdx-tabs-spacing',
},
);
4 changes: 4 additions & 0 deletions resources/eslint-internal-rules/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { mdxTabsSpacingProcessor } from './mdx-tabs-spacing.js';
import { noDirImportRule } from './no-dir-import.js';
import { onlyAsciiRule } from './only-ascii.js';
import { requireGraphqlPublicApiDocsRule } from './require-graphql-public-api-docs.js';
import { requirePublicApiExportsRule } from './require-public-api-exports.js';
import { requireToStringTagRule } from './require-to-string-tag.js';

const internalRulesPlugin = {
processors: {
'mdx-tabs-spacing': mdxTabsSpacingProcessor,
},
rules: {
...onlyAsciiRule,
...noDirImportRule,
Expand Down
208 changes: 208 additions & 0 deletions resources/eslint-internal-rules/mdx-tabs-spacing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* Nextra <Tabs> blocks mix JSX tags with Markdown children. That boundary is
* fragile: MDX may accept a Tabs block where the tags, fenced code blocks, and
* following prose are adjacent, but Prettier will not necessarily recover the
* safer block structure for us.
*
* For example, this input has the unsafe adjacency we want to reject. The
* fenced JavaScript is intentionally unformatted so the Prettier behavior is
* visible:
*
* <Tabs items={["Code"]}>
* <Tabs.Tab>
* ```js
* const value={a:1};
* ```
* </Tabs.Tab></Tabs>
* Next paragraph.
*
* Running `prettier --parser mdx` on that input produces this:
*
* <Tabs items={["Code"]}>
* <Tabs.Tab>
* ```js
* const value={a:1};
* ```
* </Tabs.Tab></Tabs>
* Next paragraph.
*
* The formatting problem is that Prettier does not treat the fenced block as a
* normal Markdown code fence in a Tabs panel: the JavaScript stays unformatted,
* the fence remains glued to <Tabs.Tab>, the closing tags remain collapsed, and
* the next paragraph remains glued to </Tabs>.
*
* With clear JSX/Markdown boundaries, Prettier formats the fenced JavaScript and
* keeps the Tabs block readable:
*
* <Tabs items={["Code"]}>
* <Tabs.Tab>
*
* ```js
* const value = { a: 1 };
* ```
*
* </Tabs.Tab>
* </Tabs>
*
* Next paragraph.
*
* This is intentionally not a general Markdown style rule. It only protects the
* Nextra Tabs shapes that can survive Prettier in a bad form.
*/

const mdxSourceByFilename = new Map();
const mdxTabsSpacingProcessor = {
meta: {
name: 'internal-rules/mdx-tabs-spacing',
},
preprocess(text, filename) {
mdxSourceByFilename.set(filename, text);
return [''];
},
postprocess(messageLists, filename) {
const source = mdxSourceByFilename.get(filename);
mdxSourceByFilename.delete(filename);

return [
...messageLists.flat(),
...(source == null ? [] : checkMdxTabsSpacing(source)),
];
},
supportsAutofix: false,
};

function checkMdxTabsSpacing(source) {
const lines = source.split(/\r?\n/u);
const messages = [];

for (let index = 0; index < lines.length; index++) {
const line = lines[index];
const trimmed = line.trim();

// Prettier can collapse or preserve this one-line close in a way that makes
// later conflict resolution hard to read and easy to break again. Keeping the
// component close and container close on separate lines gives Markdown a
// clear block boundary after the final tab panel.
if (/<\/Tabs\.Tab>\s*<\/Tabs>/.test(line)) {
report(
messages,
lines,
index,
line.indexOf('</Tabs.Tab>'),
'Close </Tabs.Tab> and </Tabs> on separate lines.',
);
}

// A <Tabs> block should start as its own Markdown block. If it is glued to
// the previous paragraph/list item, MDX still accepts the file, but Prettier
// can treat the JSX and surrounding Markdown as one construct and preserve
// surprising layout.
if (isTabsOpen(trimmed) && index > 0 && !isBlank(lines[index - 1])) {
report(
messages,
lines,
index,
line.indexOf('<Tabs'),
'Add a blank line before <Tabs> blocks.',
);
}

// The closing </Tabs> needs the same protection in the other direction. The
// bug this guard was added for was exactly a closing Tabs block followed by
// prose with no blank line; that made the following paragraph part of the
// same MDX flow and led Prettier to keep an unsafe shape.
if (
isTabsClose(trimmed) &&
index < lines.length - 1 &&
!isBlank(lines[index + 1])
) {
report(
messages,
lines,
index,
line.indexOf('</Tabs>'),
'Add a blank line after </Tabs> blocks.',
);
}

// Fenced code inside a JSX child is only unambiguously Markdown when there is
// a blank line after <Tabs.Tab>. Without it, MDX/Prettier can handle the
// fence as adjacent JSX text, and later formatting may not restore the tab
// panel structure we expect.
if (
isTabsTabOpen(trimmed) &&
index < lines.length - 1 &&
isCodeFence(lines[index + 1])
) {
report(
messages,
lines,
index,
line.indexOf('<Tabs.Tab>'),
'Add a blank line between <Tabs.Tab> and fenced code blocks.',
);
}

// Likewise, the end of a fenced code block should be separated from the
// closing tab panel tag. This keeps the fence close from being visually and
// syntactically glued to JSX during conflict resolution and Prettier passes.
if (
isCodeFence(line) &&
index < lines.length - 1 &&
isTabsTabClose(lines[index + 1].trim())
) {
report(
messages,
lines,
index,
firstNonWhitespaceIndex(line),
'Add a blank line between fenced code blocks and </Tabs.Tab>.',
);
}
}

return messages;
}

function report(messages, lines, lineIndex, columnIndex, message) {
messages.push({
ruleId: 'internal-rules/mdx-tabs-spacing',
severity: 2,
message,
line: lineIndex + 1,
column: columnIndex + 1,
endLine: lineIndex + 1,
endColumn: lines[lineIndex].length + 1,
});
}

function isTabsOpen(trimmedLine) {
return /^<Tabs(?:\s|>)/u.test(trimmedLine);
}

function isTabsClose(trimmedLine) {
return trimmedLine === '</Tabs>';
}

function isTabsTabOpen(trimmedLine) {
return trimmedLine === '<Tabs.Tab>';
}

function isTabsTabClose(trimmedLine) {
return trimmedLine === '</Tabs.Tab>';
}

function isCodeFence(line) {
return /^`{3,}/u.test(line.trim());
}

function isBlank(line) {
return line.trim() === '';
}

function firstNonWhitespaceIndex(line) {
const match = /\S/u.exec(line);
return match == null ? 0 : match.index;
}

export { mdxTabsSpacingProcessor };
50 changes: 26 additions & 24 deletions website/pages/docs/advanced-custom-scalars.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ import { Callout } from 'nextra/components';

# Custom Scalars: Best Practices and Testing

Custom scalars must behave predictably and clearly. To maintain a consistent, reliable
Custom scalars must behave predictably and clearly. To maintain a consistent, reliable
schema, follow these best practices.

<Callout type="info">
GraphQL.js v17 renames the scalar hooks to the coercion terms used by the
specification. The v16 names still work in v17, but are deprecated for
removal in v18.
specification. The v16 names still work in v17, but are deprecated for removal
in v18.
</Callout>

### Map v16 names to v17 names

| v16 name | v17 name | Purpose |
| --- | --- | --- |
| `serialize` | `coerceOutputValue` | Convert resolver values into response values. |
| `parseValue` | `coerceInputValue` | Convert variable values into internal values. |
| `parseLiteral` | `coerceInputLiteral` | Convert constant GraphQL literals into internal values. |
| `astFromValue()` | `valueToLiteral()` | Convert external input values into GraphQL literals. |
| v16 name | v17 name | Purpose |
| ---------------- | -------------------- | ------------------------------------------------------- |
| `serialize` | `coerceOutputValue` | Convert resolver values into response values. |
| `parseValue` | `coerceInputValue` | Convert variable values into internal values. |
| `parseLiteral` | `coerceInputLiteral` | Convert constant GraphQL literals into internal values. |
| `astFromValue()` | `valueToLiteral()` | Convert external input values into GraphQL literals. |

If your scalar supports both v16 and v17, keep the v16 method names for now.
When your minimum version is v17, implement the v17 names and add
Expand All @@ -37,7 +37,7 @@ tooling calls `coerceInputLiteral()` directly outside execution, it must call

### Document expected formats and validation

Provide a clear description of the scalar's accepted input and output formats. For example, a
Provide a clear description of the scalar's accepted input and output formats. For example, a
`DateTime` scalar should explain that it expects [ISO-8601](https://www.iso.org/iso-8601-date-and-time-format.html) strings ending with `Z`.

Clear descriptions help clients understand valid input and reduce mistakes.
Expand Down Expand Up @@ -77,7 +77,7 @@ Clear error messages speed up debugging and make mistakes easier to fix.
### Serialize consistently

Always serialize internal values into a predictable format.
For example, a `DateTime` scalar should always produce an ISO string, even if its
For example, a `DateTime` scalar should always produce an ISO string, even if its
internal value is a `Date` object.

```js
Expand Down Expand Up @@ -167,7 +167,7 @@ async function testQuery() {
testQuery();
```

Schema-level tests verify that the scalar behaves correctly during execution, not just
Schema-level tests verify that the scalar behaves correctly during execution, not just
in isolation.

## Common use cases for custom scalars
Expand All @@ -181,7 +181,9 @@ Custom scalars solve real-world needs by handling types that built-in scalars do
function validateEmail(value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new TypeError(`Email cannot represent invalid email address: ${value}`);
throw new TypeError(
`Email cannot represent invalid email address: ${value}`,
);
}
return value;
}
Expand All @@ -200,21 +202,21 @@ function validateURL(value) {
}
```

- `JSON`: Represents arbitrary JSON structures, but use carefully because it bypasses
GraphQL's strict type checking.
- `JSON`: Represents arbitrary JSON structures, but use carefully because it bypasses
GraphQL's strict type checking.

## When to use existing libraries

Writing scalars is deceptively tricky. Validation edge cases can lead to subtle bugs if
Writing scalars is deceptively tricky. Validation edge cases can lead to subtle bugs if
not handled carefully.

Whenever possible, use trusted libraries like [`graphql-scalars`](https://www.npmjs.com/package/graphql-scalars). They offer production-ready
Whenever possible, use trusted libraries like [`graphql-scalars`](https://www.npmjs.com/package/graphql-scalars). They offer production-ready
scalars for DateTime, EmailAddress, URL, UUID, and many others.

### Example: Handling email validation

Handling email validation correctly requires dealing with Unicode, quoted local parts, and
domain validation. Rather than writing your own regex, it's better to use a library scalar
Handling email validation correctly requires dealing with Unicode, quoted local parts, and
domain validation. Rather than writing your own regex, it's better to use a library scalar
that's already validated against standards.

If you need domain-specific behavior, you can wrap an existing scalar with custom rules:
Expand Down Expand Up @@ -242,12 +244,12 @@ const StrictEmailAddress = new GraphQLScalarType({
});
```

By following these best practices and using trusted tools where needed, you can build custom
By following these best practices and using trusted tools where needed, you can build custom
scalars that are reliable, maintainable, and easy for clients to work with.

## Additional resources

- [GraphQL Scalars by The Guild](https://the-guild.dev/graphql/scalars): A production-ready
library of common custom scalars.
- [GraphQL Scalars Specification](https://github.com/graphql/graphql-scalars): This
specification is no longer actively maintained, but useful for historical context.
- [GraphQL Scalars by The Guild](https://the-guild.dev/graphql/scalars): A production-ready
library of common custom scalars.
- [GraphQL Scalars Specification](https://github.com/graphql/graphql-scalars): This
specification is no longer actively maintained, but useful for historical context.
8 changes: 6 additions & 2 deletions website/pages/docs/advanced-execution-pipelines.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ function executeWithTiming(args) {
};
};

return typeof result.then === 'function' ? result.then(addTiming) : addTiming(result);
return typeof result.then === 'function'
? result.then(addTiming)
: addTiming(result);
}
```

Expand Down Expand Up @@ -101,7 +103,9 @@ function subscribeWithHostPipeline(args) {
return mapSourceToResponseEvent(validatedArgs, sourceEventStream);
};

return typeof source.then === 'function' ? source.then(mapSource) : mapSource(source);
return typeof source.then === 'function'
? source.then(mapSource)
: mapSource(source);
}
```

Expand Down
Loading
Loading