middleware.ts 6.1 KB
Newer Older
1
import { codeFrameColumns } from '@babel/code-frame'
2
import { constants as FS, promises as fs } from 'fs'
3 4
import { IncomingMessage, ServerResponse } from 'http'
import path from 'path'
5 6 7 8 9
import {
  NullableMappedPosition,
  RawSourceMap,
  SourceMapConsumer,
} from 'source-map'
10 11
import { StackFrame } from 'stacktrace-parser'
import url from 'url'
J
Joe Haddad 已提交
12
// eslint-disable-next-line import/no-extraneous-dependencies
13
import webpack from 'webpack'
14
import { getRawSourceMap } from './internal/helpers/getRawSourceMap'
15
import { launchEditor } from './internal/helpers/launchEditor'
16

17
export type OverlayMiddlewareOptions = {
18 19
  rootDirectory: string
  stats(): webpack.Stats
20
  serverStats(): webpack.Stats
21 22 23 24 25 26 27
}

export type OriginalStackFrameResponse = {
  originalStackFrame: StackFrame
  originalCodeFrame: string | null
}

28 29
type Source = { map: () => RawSourceMap } | null

30
function getOverlayMiddleware(options: OverlayMiddlewareOptions) {
31 32 33 34 35
  async function getSourceById(
    isServerSide: boolean,
    isFile: boolean,
    id: string
  ): Promise<Source> {
36
    if (isFile) {
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
      const fileContent: string | null = await fs
        .readFile(id, 'utf-8')
        .catch(() => null)

      if (fileContent == null) {
        return null
      }

      const map = getRawSourceMap(fileContent)
      if (map == null) {
        return null
      }

      return {
        map() {
          return map
        },
      }
    }

57
    try {
58 59 60
      const compilation = isServerSide
        ? options.serverStats()?.compilation
        : options.stats()?.compilation
J
Joe Haddad 已提交
61
      const m = compilation?.modules?.find((m) => m.id === id)
62 63 64 65 66 67 68 69 70 71 72 73
      return (
        m?.source(
          compilation.dependencyTemplates,
          compilation.runtimeTemplate
        ) ?? null
      )
    } catch (err) {
      console.error(`Failed to lookup module by ID ("${id}"):`, err)
      return null
    }
  }

J
Joe Haddad 已提交
74
  return async function (
75 76 77 78 79 80 81
    req: IncomingMessage,
    res: ServerResponse,
    next: Function
  ) {
    const { pathname, query } = url.parse(req.url, true)

    if (pathname === '/__nextjs_original-stack-frame') {
82 83 84
      const frame = (query as unknown) as StackFrame & {
        isServerSide: 'true' | 'false'
      }
85
      if (
86 87 88 89
        !(
          (frame.file?.startsWith('webpack-internal:///') ||
            frame.file?.startsWith('file://')) &&
          Boolean(parseInt(frame.lineNumber?.toString() ?? '', 10))
90
        )
91 92 93 94 95 96
      ) {
        res.statusCode = 400
        res.write('Bad Request')
        return res.end()
      }

97
      const isServerSide = frame.isServerSide === 'true'
98 99 100 101
      const moduleId: string = frame.file.replace(
        /^(webpack-internal:\/\/\/|file:\/\/)/,
        ''
      )
102 103 104

      let source: Source
      try {
105 106 107 108 109
        source = await getSourceById(
          isServerSide,
          frame.file.startsWith('file:'),
          moduleId
        )
110 111 112 113 114 115 116 117
      } catch (err) {
        console.log('Failed to get source map:', err)
        res.statusCode = 500
        res.write('Internal Server Error')
        return res.end()
      }

      if (source == null) {
118 119
        res.statusCode = 204
        res.write('No Content')
120 121
        return res.end()
      }
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147

      const frameLine = parseInt(frame.lineNumber?.toString() ?? '', 10)
      let frameColumn: number | null = parseInt(
        frame.column?.toString() ?? '',
        10
      )
      if (!frameColumn) {
        frameColumn = null
      }

      let pos: NullableMappedPosition
      try {
        const consumer = await new SourceMapConsumer(source.map())
        pos = consumer.originalPositionFor({
          line: frameLine,
          column: frameColumn,
        })
        consumer.destroy()
      } catch (err) {
        console.log('Failed to parse source map:', err)
        res.statusCode = 500
        res.write('Internal Server Error')
        return res.end()
      }

      if (pos.source == null) {
148 149
        res.statusCode = 204
        res.write('No Content')
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
        return res.end()
      }

      const filePath = path.resolve(
        options.rootDirectory,
        pos.source.startsWith('webpack:///')
          ? pos.source.substring(11)
          : pos.source
      )
      const fileContent: string | null = await fs
        .readFile(filePath, 'utf-8')
        .catch(() => null)

      const originalFrame: StackFrame = {
        file: fileContent
          ? path.relative(options.rootDirectory, filePath)
          : pos.source,
        lineNumber: pos.line,
        column: pos.column,
        methodName: frame.methodName, // TODO: resolve original method name (?)
        arguments: [],
      }

      const originalCodeFrame: string | null =
        fileContent && pos.line
          ? (codeFrameColumns(
              fileContent,
              { start: { line: pos.line, column: pos.column } },
              { forceColor: true }
            ) as string)
          : null

      const o: OriginalStackFrameResponse = {
        originalStackFrame: originalFrame,
        originalCodeFrame,
      }
      res.statusCode = 200
      res.setHeader('Content-Type', 'application/json')
      res.write(Buffer.from(JSON.stringify(o)))
189
      return res.end()
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
    } else if (pathname === '/__nextjs_launch-editor') {
      const frame = (query as unknown) as StackFrame

      const frameFile = frame.file?.toString() || null
      if (frameFile == null) {
        res.statusCode = 400
        res.write('Bad Request')
        return res.end()
      }

      const filePath = path.resolve(options.rootDirectory, frameFile)
      const fileExists = await fs.access(filePath, FS.F_OK).then(
        () => true,
        () => false
      )
      if (!fileExists) {
206 207
        res.statusCode = 204
        res.write('No Content')
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
        return res.end()
      }

      const frameLine = parseInt(frame.lineNumber?.toString() ?? '', 10) || 1
      const frameColumn = parseInt(frame.column?.toString() ?? '', 10) || 1

      try {
        await launchEditor(filePath, frameLine, frameColumn)
      } catch (err) {
        console.log('Failed to launch editor:', err)
        res.statusCode = 500
        res.write('Internal Server Error')
        return res.end()
      }

      res.statusCode = 204
      return res.end()
225 226 227 228 229
    }
    return next()
  }
}

230
export { getOverlayMiddleware }