Repository: vercel/next.js
Pull request: Add test for twoslash

To be improved by better NFT....

Closes PACK-5605

Changed files:
filename: test/e2e/twoslash/app/route.js
patch: @@ -0,0 +1,24 @@
+import { createTwoslasher } from 'twoslash'
+import ts from 'typescript'
+
+export function GET(request) {
+  try {
+    const options = request.nextUrl.searchParams.has('esnext')
+      ? {
+          target: ts.ScriptTarget.ESNext,
+          lib: ['ESNext', 'DOM', 'esnext', 'dom', 'es2020'],
+        }
+      : {}
+
+    const code = `type X = Promise<number>;
+'hello'.toUpperCase()`
+    const twoslasher = createTwoslasher({
+      compilerOptions: options,
+    })
+    const result = twoslasher(code)
+
+    return Response.json(result)
+  } catch (e) {
+    return Response.json({ error: e })
+  }
+}

filename: test/e2e/twoslash/index.test.ts
patch: @@ -0,0 +1,55 @@
+import { nextTestSetup } from 'e2e-utils'
+
+describe('twoslash', () => {
+  const { next } = nextTestSetup({
+    files: __dirname,
+    dependencies: {
+      twoslash: '0.3.4',
+    },
+  })
+
+  it.each(['default', 'esnext'])(
+    'should annotate twoslash types %s',
+    async (mode) => {
+      const { code, nodes, error } = JSON.parse(await next.render(`/?${mode}`))
+      expect({ code, nodes, error }).toMatchInlineSnapshot(`
+     {
+       "code": "type X = Promise<number>;
+     'hello'.toUpperCase()",
+       "error": undefined,
+       "nodes": [
+         {
+           "character": 5,
+           "length": 1,
+           "line": 0,
+           "start": 5,
+           "target": "X",
+           "text": "type X = Promise<number>",
+           "type": "hover",
+         },
+         {
+           "character": 9,
+           "docs": "Represents the completion of an asynchronous operation",
+           "length": 7,
+           "line": 0,
+           "start": 9,
+           "target": "Promise",
+           "text": "interface Promise<T>",
+           "type": "hover",
+         },
+         {
+           "character": 8,
+           "docs": "Converts all the alphabetic characters in a string to uppercase.",
+           "length": 11,
+           "line": 1,
+           "start": 34,
+           "target": "toUpperCase",
+           "text": "(method) String.toUpperCase(): string",
+           "type": "hover",
+         },
+       ],
+     }
+    `)
+    }
+  )
+})

filename: test/e2e/twoslash/next.config.js
patch: @@ -0,0 +1,26 @@
+const path = require('path')
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  // output: 'standalone',
+
+  // TODO ideally, all of these manual includes wouldn't be necessary. Maybe we can improve that for
+  // Turbopack
+  outputFileTracingIncludes: {
+    '/': [
+      path.relative(
+        process.cwd(),
+        path.resolve(
+          require.resolve('typescript/package.json'),
+          '..',
+          'lib',
+          'lib.*.d.ts'
+        )
+      ),
+      './node_modules/@types/node/**',
+    ],
+  },
+  serverExternalPackages: ['twoslash'],
+}
+
+module.exports = nextConfig

filename: test/e2e/twoslash/standalone.test.ts
patch: @@ -0,0 +1,113 @@
+import { nextTestSetup } from 'e2e-utils'
+import fs from 'fs-extra'
+import os from 'os'
+import path from 'path'
+import {
+  findPort,
+  initNextServerScript,
+  killApp,
+  fetchViaHTTP,
+} from 'next-test-utils'
+
+if (!(globalThis as any).isNextStart) {
+  it('should skip for non-next start', () => {})
+} else {
+  describe('output: standalone with twoslash', () => {
+    const { next, skipped } = nextTestSetup({
+      files: __dirname,
+      dependencies: {
+        twoslash: '0.3.4',
+      },
+      skipStart: true,
+    })
+
+    if (skipped) {
+      return
+    }
+
+    let server: any
+    let appPort: number
+    let tmpFolder: string
+
+    beforeAll(async () => {
+      await next.patchFile(
+        'next.config.js',
+        (await next.readFile('next.config.js')).replace('// output', 'output')
+      )
+      await next.build()
+
+      tmpFolder = path.join(os.tmpdir(), 'next-standalone-' + Date.now())
+      await fs.mkdirp(tmpFolder)
+      const distFolder = path.join(tmpFolder, 'test')
+      await fs.move(path.join(next.testDir, '.next/standalone'), distFolder)
+      const testServer = path.join(distFolder, 'server.js')
+      appPort = await findPort()
+      server = await initNextServerScript(
+        testServer,
+        /- Local:/,
+        {
+          ...process.env,
+          PORT: appPort.toString(),
+        },
+        undefined,
+        {
+          cwd: distFolder,
+        }
+      )
+    })
+
+    afterAll(async () => {
+      if (server) await killApp(server)
+      if (!process.env.NEXT_TEST_SKIP_CLEANUP) {
+        await fs.remove(tmpFolder)
+      }
+    })
+
+    it.each(['default', 'esnext'])(
+      'should annotate twoslash types %s',
+      async (mode) => {
+        const { code, nodes, error } = await (
+          await fetchViaHTTP(appPort, `/?${mode}`)
+        ).json()
+        expect({ code, nodes, error }).toMatchInlineSnapshot(`
+           {
+             "code": "type X = Promise<number>;
+           'hello'.toUpperCase()",
+             "error": undefined,
+             "nodes": [
+               {
+                 "character": 5,
+                 "length": 1,
+                 "line": 0,
+                 "start": 5,
+                 "target": "X",
+                 "text": "type X = Promise<number>",
+                 "type": "hover",
+               },
+               {
+                 "character": 9,
+                 "docs": "Represents the completion of an asynchronous operation",
+                 "length": 7,
+                 "line": 0,
+                 "start": 9,
+                 "target": "Promise",
+                 "text": "interface Promise<T>",
+                 "type": "hover",
+               },
+               {
+                 "character": 8,
+                 "docs": "Converts all the alphabetic characters in a string to uppercase.",
+                 "length": 11,
+                 "line": 1,
+                 "start": 34,
+                 "target": "toUpperCase",
+                 "text": "(method) String.toUpperCase(): string",
+                 "type": "hover",
+               },
+             ],
+           }
+          `)
+      }
+    )
+  })
+}