Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto completion for prop names and types to the TS plugin #43909

Merged
merged 3 commits into from Dec 10, 2022
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
99 changes: 98 additions & 1 deletion packages/next/server/next-typescript.ts
Expand Up @@ -203,6 +203,10 @@ export function createTSPlugin(modules: {
'^' + (projectDir + '(/src)?/app').replace(/[\\/]/g, '[\\/]')
)

const isPositionInsideNode = (position: number, node: ts.Node) => {
const start = node.getFullStart()
return start <= position && position <= node.getFullWidth() + start
}
const isAppEntryFile = (filePath: string) => {
return (
appDir.test(filePath) &&
Expand Down Expand Up @@ -388,6 +392,99 @@ export function createTSPlugin(modules: {
] as ts.CompletionEntry[]
})

const program = info.languageService.getProgram()
const source = program?.getSourceFile(fileName)
if (!source || !program) return prior

ts.forEachChild(source!, (node) => {
// Auto completion for default export function's props.
if (
isDefaultFunctionExport(node) &&
isPositionInsideNode(position, node)
) {
const paramNode = (node as ts.FunctionDeclaration).parameters?.[0]
if (isPositionInsideNode(position, paramNode)) {
const props = paramNode?.name
if (props && ts.isObjectBindingPattern(props)) {
let validProps = []
let validPropsWithType = []
let type: string

if (isPageFile(fileName)) {
// For page entries (page.js), it can only have `params` and `searchParams`
// as the prop names.
validProps = ALLOWED_PAGE_PROPS
validPropsWithType = ALLOWED_PAGE_PROPS
type = 'page'
} else {
// For layout entires, check if it has any named slots.
const currentDir = path.dirname(fileName)
const items = fs.readdirSync(currentDir, {
withFileTypes: true,
})
const slots = []
for (const item of items) {
if (item.isDirectory() && item.name.startsWith('@')) {
slots.push(item.name.slice(1))
}
}
validProps = ALLOWED_LAYOUT_PROPS.concat(slots)
validPropsWithType = ALLOWED_LAYOUT_PROPS.concat(
slots.map((s) => `${s}: React.ReactNode`)
)
type = 'layout'
}

// Auto completion for props
for (const element of props.elements) {
if (isPositionInsideNode(position, element)) {
const nameNode = element.propertyName || element.name

if (isPositionInsideNode(position, nameNode)) {
for (const name of validProps) {
prior.entries.push({
name,
insertText: name,
sortText: '_' + name,
kind: ts.ScriptElementKind.memberVariableElement,
kindModifiers: ts.ScriptElementKindModifier.none,
labelDetails: {
description: `Next.js ${type} prop`,
},
} as ts.CompletionEntry)
}
}

break
}
}

// Auto completion for types
if (paramNode.type && ts.isTypeLiteralNode(paramNode.type)) {
for (const member of paramNode.type.members) {
if (isPositionInsideNode(position, member)) {
for (const name of validPropsWithType) {
prior.entries.push({
name,
insertText: name,
sortText: '_' + name,
kind: ts.ScriptElementKind.memberVariableElement,
kindModifiers: ts.ScriptElementKindModifier.none,
labelDetails: {
description: `Next.js ${type} prop type`,
},
} as ts.CompletionEntry)
}

break
}
}
}
}
}
}
})

return prior
}

Expand Down Expand Up @@ -732,7 +829,7 @@ export function createTSPlugin(modules: {
const props = (node as ts.FunctionDeclaration).parameters?.[0]?.name
if (props && ts.isObjectBindingPattern(props)) {
for (const prop of (props as ts.ObjectBindingPattern).elements) {
const propName = prop.name.getText()
const propName = (prop.propertyName || prop.name).getText()
if (!validProps.includes(propName)) {
prior.push({
file: source,
Expand Down