Skip to content

Commit

Permalink
Add auto completion for prop names and types to the TS plugin (#43909)
Browse files Browse the repository at this point in the history
For example, [named
slots](https://nextjs.org/blog/layouts-rfc#convention:~:text=After%20this%20change%2C%20the%20layout%20will%20receive%20a%20prop%20called%C2%A0customProp%C2%A0instead%20of%C2%A0children.)
should be hinted when typing:

```ts
export default function Layout({ f|
                                  ^foo
```

And the prop type:

```ts
export default function Layout({ foo }: { f|
                                           ^foo: React.ReactChildren
```

And
[params](https://beta.nextjs.org/docs/api-reference/file-conventions/page#params-optional):

```ts
export default function Page({ p|
```

NEXT-178

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
shuding and kodiakhq[bot] committed Dec 10, 2022
1 parent 3833aed commit 1fb4cad
Showing 1 changed file with 98 additions and 1 deletion.
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

0 comments on commit 1fb4cad

Please sign in to comment.