Skip to content
Open
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
24 changes: 24 additions & 0 deletions examples/nuxt-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
logs
*.log

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example
75 changes: 75 additions & 0 deletions examples/nuxt-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Nuxt Minimal Starter

Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.

## Setup

Make sure to install dependencies:

```bash
# npm
npm install

# pnpm
pnpm install

# yarn
yarn install

# bun
bun install
```

## Development Server

Start the development server on `http://localhost:3000`:

```bash
# npm
npm run dev

# pnpm
pnpm dev

# yarn
yarn dev

# bun
bun run dev
```

## Production

Build the application for production:

```bash
# npm
npm run build

# pnpm
pnpm build

# yarn
yarn build

# bun
bun run build
```

Locally preview production build:

```bash
# npm
npm run preview

# pnpm
pnpm preview

# yarn
yarn preview

# bun
bun run preview
```

Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
6 changes: 6 additions & 0 deletions examples/nuxt-app/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
</template>
5 changes: 5 additions & 0 deletions examples/nuxt-app/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
})
17 changes: 17 additions & 0 deletions examples/nuxt-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"nuxt": "^3.16.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
}
}
Binary file added examples/nuxt-app/public/favicon.ico
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: let's remove this and some other unnecessary template files: README.md, robots.txt

Binary file not shown.
2 changes: 2 additions & 0 deletions examples/nuxt-app/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-Agent: *
Disallow:
3 changes: 3 additions & 0 deletions examples/nuxt-app/server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}
4 changes: 4 additions & 0 deletions examples/nuxt-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}
1 change: 1 addition & 0 deletions packages/steiger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"patronum": "^2.3.0",
"picocolors": "^1.1.1",
"prexit": "^2.3.0",
"tsconfck": "^3.1.4",
"yargs": "^17.7.2",
"zod": "^3.24.1",
"zod-validation-error": "^3.4.0"
Expand Down
41 changes: 23 additions & 18 deletions packages/steiger/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { $enabledRules, getEnabledRules, getGlobalIgnores } from './models/confi
import { runRule } from './features/run-rule'
import { removeGlobalIgnoreFromVfs } from './features/remove-global-ignores-from-vfs'
import { calculateFinalSeverities } from './features/calculate-diagnostic-severities'
import { handleError } from './features/handle-error'

// TODO: make this part of a plugin
function getRuleDescriptionUrl(ruleName: string) {
Expand All @@ -18,26 +19,30 @@ function getRuleDescriptionUrl(ruleName: string) {
}

async function runRules({ vfs, rules }: { vfs: Folder; rules: Array<Rule> }) {
const vfsWithoutGlobalIgnores = removeGlobalIgnoreFromVfs(vfs, getGlobalIgnores())
try {
const vfsWithoutGlobalIgnores = removeGlobalIgnoreFromVfs(vfs, getGlobalIgnores())

const ruleResults = await Promise.all(rules.map((rule) => runRule(vfsWithoutGlobalIgnores, rule)))
return ruleResults.flatMap(({ diagnostics }, ruleResultsIndex) => {
const ruleName = rules[ruleResultsIndex].name
const severities = calculateFinalSeverities(
vfsWithoutGlobalIgnores,
ruleName,
diagnostics.map((d) => d.location.path),
)

return diagnostics
.sort((a, b) => a.location.path.localeCompare(b.location.path))
.map((d, index) => ({
...d,
const ruleResults = await Promise.all(rules.map((rule) => runRule(vfsWithoutGlobalIgnores, rule)))
return ruleResults.flatMap(({ diagnostics }, ruleResultsIndex) => {
const ruleName = rules[ruleResultsIndex].name
const severities = calculateFinalSeverities(
vfsWithoutGlobalIgnores,
ruleName,
getRuleDescriptionUrl,
severity: severities[index],
}))
})
diagnostics.map((d) => d.location.path),
)

return diagnostics
.sort((a, b) => a.location.path.localeCompare(b.location.path))
.map((d, index) => ({
...d,
ruleName,
getRuleDescriptionUrl,
severity: severities[index],
}))
})
} catch (error) {
handleError(error, { vfs })
}
}

export const linter = {
Expand Down
48 changes: 48 additions & 0 deletions packages/steiger/src/features/handle-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { readFileSync } from 'fs'
import { join } from 'path'
import { TSConfckParseError } from 'tsconfck'
import { Folder } from '@steiger/types'

interface ErrorContext {
vfs: Folder
}

interface PackageJson {
dependencies?: Record<string, string>
devDependencies?: Record<string, string>
}

const readPackageJSON = (path: string) => JSON.parse(readFileSync(join(path, 'package.json'), 'utf-8')) as PackageJson
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: it might be the case that package.json is in one of the parent folders, not necessarily the one where Steiger was run

suggestion: let's use empathic to locate the package.json file:

import * as find from 'empathic/find'
find.up('package.json', { cwd: path })


async function checkNuxtConfigError(error: unknown, { vfs }: ErrorContext) {
if (!(error instanceof TSConfckParseError)) return false
if (error.code !== 'EXTENDS_RESOLVE') return false
if (!error.cause?.message?.includes('.nuxt/tsconfig')) return false

try {
const projectRootPath = vfs.path
const packageJson = readPackageJSON(projectRootPath)
const hasNuxt = !!(packageJson.dependencies?.nuxt || packageJson.devDependencies?.nuxt)
if (!hasNuxt) return false

console.error('\n\x1b[31mError: Unable to find Nuxt TypeScript configuration\x1b[0m')
console.error('\nThis appears to be a Nuxt project, but the TypeScript configuration files are missing.')
console.error('These files are auto-generated when you install dependencies.')
console.error('\nTo fix this:')
console.error('1. Run \x1b[36mnpm install\x1b[0m or \x1b[36myarn\x1b[0m or \x1b[36mpnpm install\x1b[0m')
console.error('2. Then run Steiger again\n')
Comment on lines +28 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: we have picocolors installed, let's use that instead of raw ANSI codes for text coloring for better readability

return true
} catch {
return false
}
}

export function handleError(error: unknown, context: ErrorContext): never {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: it takes a bit of code reading to understand what this function does, especially since its return type is never

suggestion: let's add a docstring that explains that this function attempts to gracefully handle the error using one of the known error checkers, and rethrows it if it can't

const isHandled = [checkNuxtConfigError].some((checker) => checker(error, context))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: interesting architecture! I'm not sure how many more of those there will be, but thanks for building with extensibility in mind :)


if (isHandled) {
process.exit(1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: not a big fan of handling process exits from within the code of the linter app. This is more the responsibility of the CLI — to print the error message and exit with an error code

suggestion: maybe we can redo this function to instead throw a known error, and also export a separate function that would handle the error message printing? The way I see it working is like this:

  1. The linter code encounters a tsconfig error
  2. The handleError function recognizes that this is a Nuxt-related issue
  3. The linter code throws a KnownIssueError (or something) to the caller, in this case, cli.ts
  4. The CLI uses the the code from the handle-error feature to print some guidance
  5. The CLI returns with a unique error code

In this solution, I think it would also make sense to make handle-error a folder, and maybe also give it a more descriptive name like detect-known-exceptions

}

throw error
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.