Skip to content

Commit 5308e8c

Browse files
authored
internal: use prettier for non-generated website files (#4809)
1 parent d7c2409 commit 5308e8c

43 files changed

Lines changed: 1087 additions & 704 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.prettierignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
# additive to .gitignore
2-
/website/**/*.mdx
2+
/website/next-env.d.ts
3+
/website/pages/api-v16/**
4+
/website/pages/api-v17/**

eslint.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,4 +1296,8 @@ export default defineConfig(
12961296
'import/unambiguous': 'off',
12971297
},
12981298
},
1299+
{
1300+
files: ['**/*.mdx'],
1301+
processor: 'internal-rules/mdx-tabs-spacing',
1302+
},
12991303
);

resources/eslint-internal-rules/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import { mdxTabsSpacingProcessor } from './mdx-tabs-spacing.js';
12
import { noDirImportRule } from './no-dir-import.js';
23
import { onlyAsciiRule } from './only-ascii.js';
34
import { requireGraphqlPublicApiDocsRule } from './require-graphql-public-api-docs.js';
45
import { requirePublicApiExportsRule } from './require-public-api-exports.js';
56
import { requireToStringTagRule } from './require-to-string-tag.js';
67

78
const internalRulesPlugin = {
9+
processors: {
10+
'mdx-tabs-spacing': mdxTabsSpacingProcessor,
11+
},
812
rules: {
913
...onlyAsciiRule,
1014
...noDirImportRule,
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* Nextra <Tabs> blocks mix JSX tags with Markdown children. That boundary is
3+
* fragile: MDX may accept a Tabs block where the tags, fenced code blocks, and
4+
* following prose are adjacent, but Prettier will not necessarily recover the
5+
* safer block structure for us.
6+
*
7+
* For example, this input has the unsafe adjacency we want to reject. The
8+
* fenced JavaScript is intentionally unformatted so the Prettier behavior is
9+
* visible:
10+
*
11+
* <Tabs items={["Code"]}>
12+
* <Tabs.Tab>
13+
* ```js
14+
* const value={a:1};
15+
* ```
16+
* </Tabs.Tab></Tabs>
17+
* Next paragraph.
18+
*
19+
* Running `prettier --parser mdx` on that input produces this:
20+
*
21+
* <Tabs items={["Code"]}>
22+
* <Tabs.Tab>
23+
* ```js
24+
* const value={a:1};
25+
* ```
26+
* </Tabs.Tab></Tabs>
27+
* Next paragraph.
28+
*
29+
* The formatting problem is that Prettier does not treat the fenced block as a
30+
* normal Markdown code fence in a Tabs panel: the JavaScript stays unformatted,
31+
* the fence remains glued to <Tabs.Tab>, the closing tags remain collapsed, and
32+
* the next paragraph remains glued to </Tabs>.
33+
*
34+
* With clear JSX/Markdown boundaries, Prettier formats the fenced JavaScript and
35+
* keeps the Tabs block readable:
36+
*
37+
* <Tabs items={["Code"]}>
38+
* <Tabs.Tab>
39+
*
40+
* ```js
41+
* const value = { a: 1 };
42+
* ```
43+
*
44+
* </Tabs.Tab>
45+
* </Tabs>
46+
*
47+
* Next paragraph.
48+
*
49+
* This is intentionally not a general Markdown style rule. It only protects the
50+
* Nextra Tabs shapes that can survive Prettier in a bad form.
51+
*/
52+
53+
const mdxSourceByFilename = new Map();
54+
const mdxTabsSpacingProcessor = {
55+
meta: {
56+
name: 'internal-rules/mdx-tabs-spacing',
57+
},
58+
preprocess(text, filename) {
59+
mdxSourceByFilename.set(filename, text);
60+
return [''];
61+
},
62+
postprocess(messageLists, filename) {
63+
const source = mdxSourceByFilename.get(filename);
64+
mdxSourceByFilename.delete(filename);
65+
66+
return [
67+
...messageLists.flat(),
68+
...(source == null ? [] : checkMdxTabsSpacing(source)),
69+
];
70+
},
71+
supportsAutofix: false,
72+
};
73+
74+
function checkMdxTabsSpacing(source) {
75+
const lines = source.split(/\r?\n/u);
76+
const messages = [];
77+
78+
for (let index = 0; index < lines.length; index++) {
79+
const line = lines[index];
80+
const trimmed = line.trim();
81+
82+
// Prettier can collapse or preserve this one-line close in a way that makes
83+
// later conflict resolution hard to read and easy to break again. Keeping the
84+
// component close and container close on separate lines gives Markdown a
85+
// clear block boundary after the final tab panel.
86+
if (/<\/Tabs\.Tab>\s*<\/Tabs>/.test(line)) {
87+
report(
88+
messages,
89+
lines,
90+
index,
91+
line.indexOf('</Tabs.Tab>'),
92+
'Close </Tabs.Tab> and </Tabs> on separate lines.',
93+
);
94+
}
95+
96+
// A <Tabs> block should start as its own Markdown block. If it is glued to
97+
// the previous paragraph/list item, MDX still accepts the file, but Prettier
98+
// can treat the JSX and surrounding Markdown as one construct and preserve
99+
// surprising layout.
100+
if (isTabsOpen(trimmed) && index > 0 && !isBlank(lines[index - 1])) {
101+
report(
102+
messages,
103+
lines,
104+
index,
105+
line.indexOf('<Tabs'),
106+
'Add a blank line before <Tabs> blocks.',
107+
);
108+
}
109+
110+
// The closing </Tabs> needs the same protection in the other direction. The
111+
// bug this guard was added for was exactly a closing Tabs block followed by
112+
// prose with no blank line; that made the following paragraph part of the
113+
// same MDX flow and led Prettier to keep an unsafe shape.
114+
if (
115+
isTabsClose(trimmed) &&
116+
index < lines.length - 1 &&
117+
!isBlank(lines[index + 1])
118+
) {
119+
report(
120+
messages,
121+
lines,
122+
index,
123+
line.indexOf('</Tabs>'),
124+
'Add a blank line after </Tabs> blocks.',
125+
);
126+
}
127+
128+
// Fenced code inside a JSX child is only unambiguously Markdown when there is
129+
// a blank line after <Tabs.Tab>. Without it, MDX/Prettier can handle the
130+
// fence as adjacent JSX text, and later formatting may not restore the tab
131+
// panel structure we expect.
132+
if (
133+
isTabsTabOpen(trimmed) &&
134+
index < lines.length - 1 &&
135+
isCodeFence(lines[index + 1])
136+
) {
137+
report(
138+
messages,
139+
lines,
140+
index,
141+
line.indexOf('<Tabs.Tab>'),
142+
'Add a blank line between <Tabs.Tab> and fenced code blocks.',
143+
);
144+
}
145+
146+
// Likewise, the end of a fenced code block should be separated from the
147+
// closing tab panel tag. This keeps the fence close from being visually and
148+
// syntactically glued to JSX during conflict resolution and Prettier passes.
149+
if (
150+
isCodeFence(line) &&
151+
index < lines.length - 1 &&
152+
isTabsTabClose(lines[index + 1].trim())
153+
) {
154+
report(
155+
messages,
156+
lines,
157+
index,
158+
firstNonWhitespaceIndex(line),
159+
'Add a blank line between fenced code blocks and </Tabs.Tab>.',
160+
);
161+
}
162+
}
163+
164+
return messages;
165+
}
166+
167+
function report(messages, lines, lineIndex, columnIndex, message) {
168+
messages.push({
169+
ruleId: 'internal-rules/mdx-tabs-spacing',
170+
severity: 2,
171+
message,
172+
line: lineIndex + 1,
173+
column: columnIndex + 1,
174+
endLine: lineIndex + 1,
175+
endColumn: lines[lineIndex].length + 1,
176+
});
177+
}
178+
179+
function isTabsOpen(trimmedLine) {
180+
return /^<Tabs(?:\s|>)/u.test(trimmedLine);
181+
}
182+
183+
function isTabsClose(trimmedLine) {
184+
return trimmedLine === '</Tabs>';
185+
}
186+
187+
function isTabsTabOpen(trimmedLine) {
188+
return trimmedLine === '<Tabs.Tab>';
189+
}
190+
191+
function isTabsTabClose(trimmedLine) {
192+
return trimmedLine === '</Tabs.Tab>';
193+
}
194+
195+
function isCodeFence(line) {
196+
return /^`{3,}/u.test(line.trim());
197+
}
198+
199+
function isBlank(line) {
200+
return line.trim() === '';
201+
}
202+
203+
function firstNonWhitespaceIndex(line) {
204+
const match = /\S/u.exec(line);
205+
return match == null ? 0 : match.index;
206+
}
207+
208+
export { mdxTabsSpacingProcessor };

website/pages/docs/advanced-custom-scalars.mdx

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ import { Callout } from 'nextra/components';
66

77
# Custom Scalars: Best Practices and Testing
88

9-
Custom scalars must behave predictably and clearly. To maintain a consistent, reliable
9+
Custom scalars must behave predictably and clearly. To maintain a consistent, reliable
1010
schema, follow these best practices.
1111

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

1818
### Map v16 names to v17 names
1919

20-
| v16 name | v17 name | Purpose |
21-
| --- | --- | --- |
22-
| `serialize` | `coerceOutputValue` | Convert resolver values into response values. |
23-
| `parseValue` | `coerceInputValue` | Convert variable values into internal values. |
24-
| `parseLiteral` | `coerceInputLiteral` | Convert constant GraphQL literals into internal values. |
25-
| `astFromValue()` | `valueToLiteral()` | Convert external input values into GraphQL literals. |
20+
| v16 name | v17 name | Purpose |
21+
| ---------------- | -------------------- | ------------------------------------------------------- |
22+
| `serialize` | `coerceOutputValue` | Convert resolver values into response values. |
23+
| `parseValue` | `coerceInputValue` | Convert variable values into internal values. |
24+
| `parseLiteral` | `coerceInputLiteral` | Convert constant GraphQL literals into internal values. |
25+
| `astFromValue()` | `valueToLiteral()` | Convert external input values into GraphQL literals. |
2626

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

3838
### Document expected formats and validation
3939

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

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

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

8383
```js
@@ -167,7 +167,7 @@ async function testQuery() {
167167
testQuery();
168168
```
169169

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

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

203-
- `JSON`: Represents arbitrary JSON structures, but use carefully because it bypasses
204-
GraphQL's strict type checking.
205+
- `JSON`: Represents arbitrary JSON structures, but use carefully because it bypasses
206+
GraphQL's strict type checking.
205207

206208
## When to use existing libraries
207209

208-
Writing scalars is deceptively tricky. Validation edge cases can lead to subtle bugs if
210+
Writing scalars is deceptively tricky. Validation edge cases can lead to subtle bugs if
209211
not handled carefully.
210212

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

214216
### Example: Handling email validation
215217

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

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

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

248250
## Additional resources
249251

250-
- [GraphQL Scalars by The Guild](https://the-guild.dev/graphql/scalars): A production-ready
251-
library of common custom scalars.
252-
- [GraphQL Scalars Specification](https://github.com/graphql/graphql-scalars): This
253-
specification is no longer actively maintained, but useful for historical context.
252+
- [GraphQL Scalars by The Guild](https://the-guild.dev/graphql/scalars): A production-ready
253+
library of common custom scalars.
254+
- [GraphQL Scalars Specification](https://github.com/graphql/graphql-scalars): This
255+
specification is no longer actively maintained, but useful for historical context.

website/pages/docs/advanced-execution-pipelines.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ function executeWithTiming(args) {
5353
};
5454
};
5555

56-
return typeof result.then === 'function' ? result.then(addTiming) : addTiming(result);
56+
return typeof result.then === 'function'
57+
? result.then(addTiming)
58+
: addTiming(result);
5759
}
5860
```
5961

@@ -101,7 +103,9 @@ function subscribeWithHostPipeline(args) {
101103
return mapSourceToResponseEvent(validatedArgs, sourceEventStream);
102104
};
103105

104-
return typeof source.then === 'function' ? source.then(mapSource) : mapSource(source);
106+
return typeof source.then === 'function'
107+
? source.then(mapSource)
108+
: mapSource(source);
105109
}
106110
```
107111

0 commit comments

Comments
 (0)