|
| 1 | +import path from 'path' |
| 2 | +import fs from 'fs' |
| 3 | + |
| 4 | +import { fileURLToPath } from 'node:url' |
| 5 | +import type { |
| 6 | + Analysis, |
| 7 | + ProblemSummary, |
| 8 | + Problem, |
| 9 | + ResolutionKind, |
| 10 | + ProblemKind, |
| 11 | +} from '@arethetypeswrong/core' |
| 12 | +import { |
| 13 | + checkTgz, |
| 14 | + summarizeProblems, |
| 15 | + getProblems, |
| 16 | +} from '@arethetypeswrong/core' |
| 17 | +import React from 'react' |
| 18 | +import { render, Text, Box, Static } from 'ink' |
| 19 | + |
| 20 | +const allResolutionKinds: ResolutionKind[] = [ |
| 21 | + 'node10', |
| 22 | + 'node16-cjs', |
| 23 | + 'node16-esm', |
| 24 | + 'bundler', |
| 25 | +] |
| 26 | + |
| 27 | +const problemEmoji: Record<ProblemKind, string> = { |
| 28 | + Wildcard: '❓', |
| 29 | + NoResolution: '💀', |
| 30 | + UntypedResolution: '❌', |
| 31 | + FalseCJS: '🎭', |
| 32 | + FalseESM: '👺', |
| 33 | + CJSResolvesToESM: '⚠️', |
| 34 | + FallbackCondition: '🐛', |
| 35 | + CJSOnlyExportsDefault: '🤨', |
| 36 | + FalseExportDefault: '❗️', |
| 37 | + UnexpectedESMSyntax: '🚭', |
| 38 | + UnexpectedCJSSyntax: '🚱', |
| 39 | +} |
| 40 | + |
| 41 | +const problemShortDescriptions: Record<ProblemKind, string> = { |
| 42 | + Wildcard: `${problemEmoji.Wildcard} Unable to check`, |
| 43 | + NoResolution: `${problemEmoji.NoResolution} Failed to resolve`, |
| 44 | + UntypedResolution: `${problemEmoji.UntypedResolution} No types`, |
| 45 | + FalseCJS: `${problemEmoji.FalseCJS} Masquerading as CJS`, |
| 46 | + FalseESM: `${problemEmoji.FalseESM} Masquerading as ESM`, |
| 47 | + CJSResolvesToESM: `${problemEmoji.CJSResolvesToESM} ESM (dynamic import only)`, |
| 48 | + FallbackCondition: `${problemEmoji.FallbackCondition} Used fallback condition`, |
| 49 | + CJSOnlyExportsDefault: `${problemEmoji.CJSOnlyExportsDefault} CJS default export`, |
| 50 | + FalseExportDefault: `${problemEmoji.FalseExportDefault} Incorrect default export`, |
| 51 | + UnexpectedESMSyntax: `${problemEmoji.UnexpectedESMSyntax} Unexpected ESM syntax`, |
| 52 | + UnexpectedCJSSyntax: `${problemEmoji.UnexpectedCJSSyntax} Unexpected CJS syntax`, |
| 53 | +} |
| 54 | + |
| 55 | +const resolutionKinds: Record<ResolutionKind, string> = { |
| 56 | + node10: 'node10', |
| 57 | + 'node16-cjs': 'node16 (from CJS)', |
| 58 | + 'node16-esm': 'node16 (from ESM)', |
| 59 | + bundler: 'bundler', |
| 60 | +} |
| 61 | + |
| 62 | +const moduleKinds = { |
| 63 | + 1: '(CJS)', |
| 64 | + 99: '(ESM)', |
| 65 | + '': '', |
| 66 | +} |
| 67 | + |
| 68 | +const __filename = fileURLToPath(import.meta.url) |
| 69 | +const __dirname = path.dirname(__filename) |
| 70 | + |
| 71 | +interface Checks { |
| 72 | + analysis: Analysis |
| 73 | + problemSummaries?: ProblemSummary[] |
| 74 | + problems?: Problem[] |
| 75 | +} |
| 76 | + |
| 77 | +const rtkPackagePath = path.join(__dirname, './package.tgz') |
| 78 | + |
| 79 | +const rtkPackageTgzBytes = fs.readFileSync(rtkPackagePath) |
| 80 | + |
| 81 | +function Header({ text, width }: { text: string; width: number | string }) { |
| 82 | + return ( |
| 83 | + <Box borderStyle="single" width={width}> |
| 84 | + <Text color="blue">{text}</Text> |
| 85 | + </Box> |
| 86 | + ) |
| 87 | +} |
| 88 | + |
| 89 | +function Traces({ |
| 90 | + analysis, |
| 91 | + subpaths, |
| 92 | +}: { |
| 93 | + analysis: Analysis |
| 94 | + subpaths: string[] |
| 95 | +}) { |
| 96 | + if (!('entrypointResolutions' in analysis)) { |
| 97 | + return null |
| 98 | + } |
| 99 | + |
| 100 | + return ( |
| 101 | + <Box flexDirection="column" width="100%"> |
| 102 | + {subpaths.map((subpath) => { |
| 103 | + const resolutionDetails = Object.entries( |
| 104 | + analysis.entrypointResolutions[subpath] |
| 105 | + ) |
| 106 | + return ( |
| 107 | + <Box width="100%" key={'traces-' + subpath} flexDirection="column"> |
| 108 | + <Text color="blue" bold> |
| 109 | + Traces for Subpath: {subpath} |
| 110 | + </Text> |
| 111 | + {resolutionDetails.map(([resolutionKind, details]) => { |
| 112 | + return ( |
| 113 | + <Box |
| 114 | + width="100%" |
| 115 | + key={`resolutionDetails-${resolutionKind}-${subpath}`} |
| 116 | + flexDirection="column" |
| 117 | + > |
| 118 | + <Text bold>{resolutionKind} Traces:</Text> |
| 119 | + <Box flexDirection="column"> |
| 120 | + {details.trace.map((traceLine, i) => { |
| 121 | + return ( |
| 122 | + <Text |
| 123 | + key={`resolutionDetails-traces-${subpath}-${resolutionKind}-${i}`} |
| 124 | + > |
| 125 | + {traceLine} |
| 126 | + </Text> |
| 127 | + ) |
| 128 | + })} |
| 129 | + </Box> |
| 130 | + </Box> |
| 131 | + ) |
| 132 | + })} |
| 133 | + </Box> |
| 134 | + ) |
| 135 | + })} |
| 136 | + </Box> |
| 137 | + ) |
| 138 | +} |
| 139 | + |
| 140 | +function ChecksTable(props: { checks?: Checks }) { |
| 141 | + if (!props.checks || !props.checks.analysis.containsTypes) { |
| 142 | + return null |
| 143 | + } |
| 144 | + |
| 145 | + const { analysis, problems, problemSummaries } = props.checks |
| 146 | + const subpaths = Object.keys(analysis.entrypointResolutions).filter( |
| 147 | + (key) => !key.includes('package.json') |
| 148 | + ) |
| 149 | + const entrypoints = subpaths.map((s) => |
| 150 | + s === '.' |
| 151 | + ? analysis.packageName |
| 152 | + : `${analysis.packageName}/${s.substring(2)}` |
| 153 | + ) |
| 154 | + |
| 155 | + const numColumns = entrypoints.length + 1 |
| 156 | + |
| 157 | + const columnWidth = `${100 / numColumns}%` |
| 158 | + |
| 159 | + return ( |
| 160 | + <Box flexDirection="column" width="100%"> |
| 161 | + <Box> |
| 162 | + <Header key={'empty'} text={''} width={columnWidth} /> |
| 163 | + {entrypoints.map((text) => { |
| 164 | + return <Header key={text} text={text} width={columnWidth} /> |
| 165 | + })} |
| 166 | + </Box> |
| 167 | + {allResolutionKinds.map((resolutionKind) => { |
| 168 | + return ( |
| 169 | + <Box key={resolutionKind} width="100%"> |
| 170 | + <Box borderStyle="single" width={columnWidth}> |
| 171 | + <Text>{resolutionKinds[resolutionKind]}</Text> |
| 172 | + </Box> |
| 173 | + {subpaths.map((subpath) => { |
| 174 | + const problemsForCell = problems?.filter( |
| 175 | + (problem) => |
| 176 | + problem.entrypoint === subpath && |
| 177 | + problem.resolutionKind === resolutionKind |
| 178 | + ) |
| 179 | + const resolution = |
| 180 | + analysis.entrypointResolutions[subpath][resolutionKind] |
| 181 | + .resolution |
| 182 | + |
| 183 | + let content: React.ReactNode |
| 184 | + |
| 185 | + if (problemsForCell?.length) { |
| 186 | + content = ( |
| 187 | + <Box flexDirection="column"> |
| 188 | + {problemsForCell.map((problem) => { |
| 189 | + return ( |
| 190 | + <Box key={problem.kind}> |
| 191 | + <Text>{problemShortDescriptions[problem.kind]}</Text> |
| 192 | + </Box> |
| 193 | + ) |
| 194 | + })} |
| 195 | + </Box> |
| 196 | + ) |
| 197 | + } else if (resolution?.isJson) { |
| 198 | + content = <Text>✅ (JSON)</Text> |
| 199 | + } else { |
| 200 | + content = ( |
| 201 | + <Text> |
| 202 | + {'✅ ' + |
| 203 | + moduleKinds[resolution?.moduleKind?.detectedKind || '']} |
| 204 | + </Text> |
| 205 | + ) |
| 206 | + } |
| 207 | + return ( |
| 208 | + <Box key={subpath} width={columnWidth} borderStyle="single"> |
| 209 | + {content} |
| 210 | + </Box> |
| 211 | + ) |
| 212 | + })} |
| 213 | + </Box> |
| 214 | + ) |
| 215 | + })} |
| 216 | + {problemSummaries?.map((summary) => { |
| 217 | + return ( |
| 218 | + <Box width="100%" key={summary.kind} flexDirection="column"> |
| 219 | + <Text color="red" bold> |
| 220 | + {summary.kind}: {summary.title} |
| 221 | + </Text> |
| 222 | + {summary.messages.map((message) => { |
| 223 | + return ( |
| 224 | + <Text key={message.messageText}>{message.messageText}</Text> |
| 225 | + ) |
| 226 | + })} |
| 227 | + </Box> |
| 228 | + ) |
| 229 | + })} |
| 230 | + <Traces analysis={analysis} subpaths={subpaths} /> |
| 231 | + </Box> |
| 232 | + ) |
| 233 | +} |
| 234 | + |
| 235 | +;(async function main() { |
| 236 | + const analysis = await checkTgz(rtkPackageTgzBytes) |
| 237 | + |
| 238 | + const checks: Checks = { |
| 239 | + analysis, |
| 240 | + problems: undefined, |
| 241 | + problemSummaries: undefined, |
| 242 | + } |
| 243 | + if ('entrypointResolutions' in analysis) { |
| 244 | + const problems = analysis.containsTypes ? getProblems(analysis) : undefined |
| 245 | + checks.problems = problems |
| 246 | + |
| 247 | + if (problems) { |
| 248 | + const problemSummaries = analysis.containsTypes |
| 249 | + ? summarizeProblems(problems, analysis) |
| 250 | + : undefined |
| 251 | + checks.problemSummaries = problemSummaries |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + // Ink will duplicate all of its output if it is longer than the terminal height. |
| 256 | + // Known bug with the underlying rendering: https://github.com/vadimdemedes/ink/issues/48 |
| 257 | + // Solution is to mark the entire content as "static" so it won't get updated, but flushed. |
| 258 | + render( |
| 259 | + <Static items={[checks]}> |
| 260 | + {(checks: Checks, index: number) => { |
| 261 | + return <ChecksTable key={`checks-${index}`} checks={checks} /> |
| 262 | + }} |
| 263 | + </Static> |
| 264 | + ) |
| 265 | + |
| 266 | + const exitCode = checks.problems?.length ?? 0 |
| 267 | + process.exit(exitCode) |
| 268 | +})() |
0 commit comments