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 arethetypeswrong automated CLI check #3294

Merged
merged 5 commits into from
Mar 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,16 @@ jobs:
fail-fast: false
matrix:
node: ['16.x']
example: ['cra4', 'cra5', 'next', 'vite', 'node-standard', 'node-esm']
example:
[
'cra4',
'cra5',
'next',
'vite',
'node-standard',
'node-esm',
'are-the-types-wrong',
]
defaults:
run:
working-directory: ./examples/publish-ci/${{ matrix.example }}
Expand Down Expand Up @@ -186,5 +195,5 @@ jobs:
- name: Build example
run: yarn build

- name: Run Playwright test
- name: Run test step
run: yarn test
35 changes: 35 additions & 0 deletions examples/publish-ci/are-the-types-wrong/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

typesversions
.cache
.yarnrc
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
*.tgz
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
diff --git a/package.json b/package.json
index 60791b6ccd3575279eddef2ac795802bd5044abd..41be518d2f21a659ca71095850cb18264a814f8d 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"prepublishOnly": "npm run tsc && npm run test"
},
"type": "module",
+ "types": "./dist/index.d.ts",
"exports": {
".": {
"development": "./src/index.ts",
268 changes: 268 additions & 0 deletions examples/publish-ci/are-the-types-wrong/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import path from 'path'
import fs from 'fs'

import { fileURLToPath } from 'node:url'
import type {
Analysis,
ProblemSummary,
Problem,
ResolutionKind,
ProblemKind,
} from '@arethetypeswrong/core'
import {
checkTgz,
summarizeProblems,
getProblems,
} from '@arethetypeswrong/core'
import React from 'react'
import { render, Text, Box, Static } from 'ink'

const allResolutionKinds: ResolutionKind[] = [
'node10',
'node16-cjs',
'node16-esm',
'bundler',
]

const problemEmoji: Record<ProblemKind, string> = {
Wildcard: '❓',
NoResolution: '💀',
UntypedResolution: '❌',
FalseCJS: '🎭',
FalseESM: '👺',
CJSResolvesToESM: '⚠️',
FallbackCondition: '🐛',
CJSOnlyExportsDefault: '🤨',
FalseExportDefault: '❗️',
UnexpectedESMSyntax: '🚭',
UnexpectedCJSSyntax: '🚱',
}

const problemShortDescriptions: Record<ProblemKind, string> = {
Wildcard: `${problemEmoji.Wildcard} Unable to check`,
NoResolution: `${problemEmoji.NoResolution} Failed to resolve`,
UntypedResolution: `${problemEmoji.UntypedResolution} No types`,
FalseCJS: `${problemEmoji.FalseCJS} Masquerading as CJS`,
FalseESM: `${problemEmoji.FalseESM} Masquerading as ESM`,
CJSResolvesToESM: `${problemEmoji.CJSResolvesToESM} ESM (dynamic import only)`,
FallbackCondition: `${problemEmoji.FallbackCondition} Used fallback condition`,
CJSOnlyExportsDefault: `${problemEmoji.CJSOnlyExportsDefault} CJS default export`,
FalseExportDefault: `${problemEmoji.FalseExportDefault} Incorrect default export`,
UnexpectedESMSyntax: `${problemEmoji.UnexpectedESMSyntax} Unexpected ESM syntax`,
UnexpectedCJSSyntax: `${problemEmoji.UnexpectedCJSSyntax} Unexpected CJS syntax`,
}

const resolutionKinds: Record<ResolutionKind, string> = {
node10: 'node10',
'node16-cjs': 'node16 (from CJS)',
'node16-esm': 'node16 (from ESM)',
bundler: 'bundler',
}

const moduleKinds = {
1: '(CJS)',
99: '(ESM)',
'': '',
}

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

interface Checks {
analysis: Analysis
problemSummaries?: ProblemSummary[]
problems?: Problem[]
}

const rtkPackagePath = path.join(__dirname, './package.tgz')

const rtkPackageTgzBytes = fs.readFileSync(rtkPackagePath)

function Header({ text, width }: { text: string; width: number | string }) {
return (
<Box borderStyle="single" width={width}>
<Text color="blue">{text}</Text>
</Box>
)
}

function Traces({
analysis,
subpaths,
}: {
analysis: Analysis
subpaths: string[]
}) {
if (!('entrypointResolutions' in analysis)) {
return null
}

return (
<Box flexDirection="column" width="100%">
{subpaths.map((subpath) => {
const resolutionDetails = Object.entries(
analysis.entrypointResolutions[subpath]
)
return (
<Box width="100%" key={'traces-' + subpath} flexDirection="column">
<Text color="blue" bold>
Traces for Subpath: {subpath}
</Text>
{resolutionDetails.map(([resolutionKind, details]) => {
return (
<Box
width="100%"
key={`resolutionDetails-${resolutionKind}-${subpath}`}
flexDirection="column"
>
<Text bold>{resolutionKind} Traces:</Text>
<Box flexDirection="column">
{details.trace.map((traceLine, i) => {
return (
<Text
key={`resolutionDetails-traces-${subpath}-${resolutionKind}-${i}`}
>
{traceLine}
</Text>
)
})}
</Box>
</Box>
)
})}
</Box>
)
})}
</Box>
)
}

function ChecksTable(props: { checks?: Checks }) {
if (!props.checks || !props.checks.analysis.containsTypes) {
return null
}

const { analysis, problems, problemSummaries } = props.checks
const subpaths = Object.keys(analysis.entrypointResolutions).filter(
(key) => !key.includes('package.json')
)
const entrypoints = subpaths.map((s) =>
s === '.'
? analysis.packageName
: `${analysis.packageName}/${s.substring(2)}`
)

const numColumns = entrypoints.length + 1

const columnWidth = `${100 / numColumns}%`

return (
<Box flexDirection="column" width="100%">
<Box>
<Header key={'empty'} text={''} width={columnWidth} />
{entrypoints.map((text) => {
return <Header key={text} text={text} width={columnWidth} />
})}
</Box>
{allResolutionKinds.map((resolutionKind) => {
return (
<Box key={resolutionKind} width="100%">
<Box borderStyle="single" width={columnWidth}>
<Text>{resolutionKinds[resolutionKind]}</Text>
</Box>
{subpaths.map((subpath) => {
const problemsForCell = problems?.filter(
(problem) =>
problem.entrypoint === subpath &&
problem.resolutionKind === resolutionKind
)
const resolution =
analysis.entrypointResolutions[subpath][resolutionKind]
.resolution

let content: React.ReactNode

if (problemsForCell?.length) {
content = (
<Box flexDirection="column">
{problemsForCell.map((problem) => {
return (
<Box key={problem.kind}>
<Text>{problemShortDescriptions[problem.kind]}</Text>
</Box>
)
})}
</Box>
)
} else if (resolution?.isJson) {
content = <Text>✅ (JSON)</Text>
} else {
content = (
<Text>
{'✅ ' +
moduleKinds[resolution?.moduleKind?.detectedKind || '']}
</Text>
)
}
return (
<Box key={subpath} width={columnWidth} borderStyle="single">
{content}
</Box>
)
})}
</Box>
)
})}
{problemSummaries?.map((summary) => {
return (
<Box width="100%" key={summary.kind} flexDirection="column">
<Text color="red" bold>
{summary.kind}: {summary.title}
</Text>
{summary.messages.map((message) => {
return (
<Text key={message.messageText}>{message.messageText}</Text>
)
})}
</Box>
)
})}
<Traces analysis={analysis} subpaths={subpaths} />
</Box>
)
}

;(async function main() {
const analysis = await checkTgz(rtkPackageTgzBytes)

const checks: Checks = {
analysis,
problems: undefined,
problemSummaries: undefined,
}
if ('entrypointResolutions' in analysis) {
const problems = analysis.containsTypes ? getProblems(analysis) : undefined
checks.problems = problems

if (problems) {
const problemSummaries = analysis.containsTypes
? summarizeProblems(problems, analysis)
: undefined
checks.problemSummaries = problemSummaries
}
}

// Ink will duplicate all of its output if it is longer than the terminal height.
// Known bug with the underlying rendering: https://github.com/vadimdemedes/ink/issues/48
// Solution is to mark the entire content as "static" so it won't get updated, but flushed.
render(
<Static items={[checks]}>
{(checks: Checks, index: number) => {
return <ChecksTable key={`checks-${index}`} checks={checks} />
}}
</Static>
)

const exitCode = checks.problems?.length ?? 0
process.exit(exitCode)
})()
25 changes: 25 additions & 0 deletions examples/publish-ci/are-the-types-wrong/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "are-the-types-wrong",
"packageManager": "yarn@3.2.4",
"type": "module",
"scripts": {
"build": "echo Nothing to build",
"test": "yarn tsx main.tsx"
},
"dependencies": {
"@reduxjs/toolkit": "^1.9.3",
"@tanstack/react-table": "^8.7.9",
"ink": "^4.0.0",
"object-hash": "^3.0.0",
"react": "^18.2.0"
},
"devDependencies": {
"@arethetypeswrong/core": "^0.0.4",
"@types/react": "^18.0.28",
"shelljs": "^0.8.5",
"tsx": "^3.12.5"
},
"resolutions": {
"@arethetypeswrong/core@^0.0.4": "patch:@arethetypeswrong/core@npm%3A0.0.4#./.yarn/patches/@arethetypeswrong-core-npm-0.0.4-edb717a66b.patch"
}
}
10 changes: 10 additions & 0 deletions examples/publish-ci/are-the-types-wrong/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,

"jsx": "react",
}
}
Loading