Skip to content

Commit 3925804

Browse files
dimitropoulosdevjiwonchoi
authored andcommitted
address completions and extract utils
1 parent 6305957 commit 3925804

File tree

12 files changed

+188
-139
lines changed

12 files changed

+188
-139
lines changed

packages/next/src/server/next.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -461,8 +461,17 @@ function createServer(
461461
process.env.TURBOPACK = '1'
462462
}
463463

464-
// The package is used as a TypeScript plugin.
465-
if (options && 'typescript' in options && 'version' in (options as any).typescript) {
464+
// The `next` package can be used as a TypeScript language server plugin.
465+
// This is a special case where we don't want to start the server.
466+
// That's why although the `options` values are technically valid,
467+
// we keep them out of the public types for `createServer` to avoid confusion.
468+
if (
469+
options &&
470+
'typescript' in options &&
471+
options.typescript &&
472+
typeof options.typescript === 'object' &&
473+
'version' in options.typescript
474+
) {
466475
return require('ts-plugin-next').createTSPlugin(options)
467476
}
468477

packages/ts-next-plugin/README.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ This is a TypeScript Language Server plugin for the Next.js app directory.
99
- Error ts(71016): the `'use client'` directive is used at the same time as `'use server'` directive
1010
- Error ts(71015): the `'use server'` directive must be above all other expressions
1111
- Error ts(71011): validates that server files can only export async functions
12-
- Error ts(71001): the following `react` and `react-dom` APIs are not allowed in Server Components: `useState`, `useEffect`, `useLayoutEffect`, `useDeferredValue`, `useImperativeHandle`, `useInsertionEffect`, `useReducer`, `useRef`, `useSyncExternalStore`, `useTransition`, `Component`, `PureComponent`, `createContext`, `createFactory`, `experimental_useOptimistic`, `useOptimistic`, and `useActionState`.
13-
- Hide autocompletions for disallowed APIs such as `useState`
14-
- Show errors if disallowed APIs such as `useState` are used
12+
- Error ts(71001): [`DISALLOWED_SERVER_REACT_APIS`](#glossary) are not allowed in Server Components.
13+
- Hide autocompletions for [`DISALLOWED_SERVER_REACT_APIS`](#glossary).
14+
- Modify completions for Next.js metadata (and show it higher up).
1515

1616
### 📺 Client Layer
1717

@@ -32,5 +32,9 @@ This is a TypeScript Language Server plugin for the Next.js app directory.
3232
- Error ts(71002): config files can only export the values: `config`, `generateStaticParams`, `metadata`, `generateMetadata`, `viewport`, and `generateViewport`.
3333
- Error ts(71012): config values must match the schema
3434
- Error ts(71013): config values must be serializable
35-
- Autocompletion and docs for configs
35+
- Adds autocompletion and docs for configs
3636
- Hover hints for configs
37+
38+
### Glossary
39+
40+
- `DISALLOWED_SERVER_REACT_APIS` refers to: `useState`, `useEffect`, `useLayoutEffect`, `useDeferredValue`, `useImperativeHandle`, `useInsertionEffect`, `useReducer`, `useRef`, `useSyncExternalStore`, `useTransition`, `Component`, `PureComponent`, `createContext`, `createFactory`, `experimental_useOptimistic`, `useOptimistic`, and `useActionState`

packages/ts-next-plugin/TSNextPlugin.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'path'
22
import ts from 'typescript'
3-
import { NEXT_TS_ERRORS } from './constant'
3+
import { NEXT_TS_ERRORS, USE_CLIENT, USE_SERVER } from './constant'
44

55
export class DirectiveError extends Error {
66
category: ts.Diagnostic['category']
@@ -92,7 +92,7 @@ export class TSNextPlugin {
9292
ts.isExpressionStatement(node) &&
9393
ts.isStringLiteral(node.expression)
9494
) {
95-
if (node.expression.text === 'use client') {
95+
if (node.expression.text === USE_CLIENT) {
9696
if (isDirective) {
9797
isClientEntry = true
9898
} else {
@@ -106,7 +106,7 @@ export class TSNextPlugin {
106106
}
107107
}
108108

109-
if (node.expression.text === 'use server') {
109+
if (node.expression.text === USE_SERVER) {
110110
if (isDirective) {
111111
isServerEntry = true
112112
} else {

packages/ts-next-plugin/constant.ts

+3
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,6 @@ export const API_DOCS: Record<string, APIDoc> = {
310310
},
311311
},
312312
} satisfies Record<string, APIDoc>
313+
314+
export const USE_CLIENT = 'use client'
315+
export const USE_SERVER = 'use server'

packages/ts-next-plugin/createTSPlugin.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const createTSPlugin: ts.server.PluginModuleFactory = () => {
88

99
const virtualFiles: Record<
1010
string,
11-
{ file: ts.IScriptSnapshot; ver: number }
11+
{ scriptSnapshot: ts.IScriptSnapshot; ver: number }
1212
> = {}
1313

1414
const getScriptVersion = info.languageServiceHost.getScriptVersion.bind(
@@ -32,9 +32,9 @@ export const createTSPlugin: ts.server.PluginModuleFactory = () => {
3232
const file = virtualFiles[fileName]
3333
if (!file) return getScriptSnapshot(fileName)
3434
tsNextPlugin.log(
35-
`[ProxiedLSHost] getScriptSnapshot(${fileName}) - ${JSON.stringify(file.file, null, 2)}`
35+
`[ProxiedLSHost] getScriptSnapshot(${fileName}) - ${JSON.stringify(file.scriptSnapshot, null, 2)}`
3636
)
37-
return file.file
37+
return file.scriptSnapshot
3838
}
3939

4040
const getScriptFileNames = info.languageServiceHost.getScriptFileNames.bind(
@@ -64,7 +64,7 @@ export const createTSPlugin: ts.server.PluginModuleFactory = () => {
6464
tsNextPlugin.log(`[ProxiedLSHost] readFile(${fileName})`)
6565
const file = virtualFiles[fileName]
6666
return file
67-
? file.file.getText(0, file.file.getLength())
67+
? file.scriptSnapshot.getText(0, file.scriptSnapshot.getLength())
6868
: readFile(fileName)
6969
}
7070

@@ -90,9 +90,9 @@ export const createTSPlugin: ts.server.PluginModuleFactory = () => {
9090
const existing = virtualFiles[fileName]
9191
if (existing) {
9292
virtualFiles[fileName].ver++
93-
virtualFiles[fileName].file = snap
93+
virtualFiles[fileName].scriptSnapshot = snap
9494
} else {
95-
virtualFiles[fileName] = { ver: 2, file: snap }
95+
virtualFiles[fileName] = { ver: 2, scriptSnapshot: snap }
9696
}
9797

9898
// This is the same function call that the Svelte TS plugin makes

packages/ts-next-plugin/proxy.ts

+29-17
Original file line numberDiff line numberDiff line change
@@ -48,30 +48,31 @@ export const createProxy = (tsNextPlugin: TSNextPlugin) => {
4848
entries: [],
4949
}
5050

51-
if (!tsNextPlugin.isAppEntryFile(fileName)) return prior
51+
if (!tsNextPlugin.isAppEntryFile(fileName)) {
52+
return prior
53+
}
5254

53-
// If it's a server entry.
5455
const { isClientEntry } = tsNextPlugin.getEntryInfo(fileName, {
5556
throwOnInvalidDirective: false,
5657
})
58+
5759
if (!isClientEntry) {
58-
// Remove specified entries from completion list
5960
prior.entries = server.filterCompletionsAtPosition(prior.entries)
6061

61-
// Provide autocompletion for metadata fields
62-
prior = metadata.filterCompletionsAtPosition(
62+
prior = metadata.modifyCompletionsAtPosition(
6363
fileName,
6464
position,
6565
options,
6666
prior
6767
)
6868
}
6969

70-
// Add auto completions for export configs.
7170
config.addCompletionsAtPosition(fileName, position, prior)
7271

7372
const source = tsNextPlugin.getSource(fileName)
74-
if (!source) return prior
73+
if (!source) {
74+
return prior
75+
}
7576

7677
ts.forEachChild(source!, (node) => {
7778
// Auto completion for default export function's props.
@@ -88,7 +89,6 @@ export const createProxy = (tsNextPlugin: TSNextPlugin) => {
8889
return prior
8990
}
9091

91-
// Show auto completion details
9292
proxy.getCompletionEntryDetails = (
9393
fileName: string,
9494
position: number,
@@ -102,7 +102,9 @@ export const createProxy = (tsNextPlugin: TSNextPlugin) => {
102102
entryName,
103103
data
104104
)
105-
if (entryCompletionEntryDetails) return entryCompletionEntryDetails
105+
if (entryCompletionEntryDetails) {
106+
return entryCompletionEntryDetails
107+
}
106108

107109
const metadataCompletionEntryDetails = metadata.getCompletionEntryDetails(
108110
fileName,
@@ -113,7 +115,9 @@ export const createProxy = (tsNextPlugin: TSNextPlugin) => {
113115
preferences,
114116
data
115117
)
116-
if (metadataCompletionEntryDetails) return metadataCompletionEntryDetails
118+
if (metadataCompletionEntryDetails) {
119+
return metadataCompletionEntryDetails
120+
}
117121

118122
return info.languageService.getCompletionEntryDetails(
119123
fileName,
@@ -126,13 +130,14 @@ export const createProxy = (tsNextPlugin: TSNextPlugin) => {
126130
)
127131
}
128132

129-
// Quick info
130133
proxy.getQuickInfoAtPosition = (fileName: string, position: number) => {
131134
const prior = info.languageService.getQuickInfoAtPosition(
132135
fileName,
133136
position
134137
)
135-
if (!tsNextPlugin.isAppEntryFile(fileName)) return prior
138+
if (!tsNextPlugin.isAppEntryFile(fileName)) {
139+
return prior
140+
}
136141

137142
// Remove type suggestions for disallowed APIs in server components.
138143
const { isClientEntry } = tsNextPlugin.getEntryInfo(fileName, {
@@ -148,20 +153,25 @@ export const createProxy = (tsNextPlugin: TSNextPlugin) => {
148153
}
149154

150155
const metadataInfo = metadata.getQuickInfoAtPosition(fileName, position)
151-
if (metadataInfo) return metadataInfo
156+
if (metadataInfo) {
157+
return metadataInfo
158+
}
152159
}
153160

154161
const overridden = config.getQuickInfoAtPosition(fileName, position)
155-
if (overridden) return overridden
162+
if (overridden) {
163+
return overridden
164+
}
156165

157166
return prior
158167
}
159168

160-
// Show errors for disallowed imports
161169
proxy.getSemanticDiagnostics = (fileName: string) => {
162170
const prior = info.languageService.getSemanticDiagnostics(fileName)
163171
const source = tsNextPlugin.getSource(fileName)
164-
if (!source) return prior
172+
if (!source) {
173+
return prior
174+
}
165175

166176
let isClientEntry = false
167177
let isServerEntry = false
@@ -342,7 +352,9 @@ export const createProxy = (tsNextPlugin: TSNextPlugin) => {
342352
fileName,
343353
position
344354
)
345-
if (metadataDefinition) return metadataDefinition
355+
if (metadataDefinition) {
356+
return metadataDefinition
357+
}
346358
}
347359

348360
return info.languageService.getDefinitionAndBoundSpan(fileName, position)

packages/ts-next-plugin/rules/client-boundary.ts

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const clientBoundary = (tsNextPlugin: TSNextPlugin) => ({
2222
}
2323
}
2424
}
25+
2526
return diagnostics
2627
},
2728

0 commit comments

Comments
 (0)