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
5 changes: 5 additions & 0 deletions .changeset/remove-cli-eslint-config-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/eslint-plugin-cli': major
---

Limit the bundled config to CLI custom rules and remove its shared ESLint dependency tree.
3 changes: 0 additions & 3 deletions .github/workflows/tests-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ jobs:
- name: Oxlint
run: pnpm lint:oxlint
if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == '26.1.0' }}
- name: ESLint compatibility
run: pnpm lint:eslint --output-style=stream
if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == '26.1.0' }}
- name: Type-check
run: pnpm nx run-many --all --target=type-check --output-style=stream
if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == '26.1.0' }}
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/tests-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ jobs:
run: pnpm prettier
- name: Oxlint
run: pnpm lint:oxlint
- name: ESLint compatibility
run: pnpm lint:eslint --output-style=stream

bundle:
name: 'Bundle'
Expand Down
6 changes: 0 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ web_modules/
# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

Expand Down Expand Up @@ -175,9 +172,6 @@ testing.mjs
# NX
.nx

# ESLint reports
eslint-report.json

# Generated docs
docs/api
docs-shopify.dev/commands/interfaces/
Expand Down
2 changes: 1 addition & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode","dbaeumer.vscode-eslint","nrwl.angular-console","vitest.explorer"]
"recommendations": ["esbenp.prettier-vscode", "oxc.oxc-vscode", "nrwl.angular-console", "vitest.explorer"]
}
5 changes: 2 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"**/node_modules": true
},
"editor.formatOnSave": false,
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"oxc.enable": true,
"[javascript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
Expand All @@ -31,9 +31,8 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
"source.fixAll.oxc": "explicit"
},
"eslint.workingDirectories": [{ "pattern": "packages/*" }],
"vitest.enable": true,
"testing.automaticallyOpenPeekView": "never",
"javascript.preferences.importModuleSpecifierEnding": "js",
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ If the change is not ready to be public, do not add a changeset.
- [docs/cli/conventions.md](docs/cli/conventions.md)
- [docs/cli/cross-os-compatibility.md](docs/cli/cross-os-compatibility.md)
- [docs/cli/debugging.md](docs/cli/debugging.md)
- [docs/cli/eslint-rules.md](docs/cli/eslint-rules.md)
- [docs/cli/oxlint-rules.md](docs/cli/oxlint-rules.md)
- [docs/cli/faq.md](docs/cli/faq.md)
- [docs/cli/get-started.md](docs/cli/get-started.md)
- [docs/cli/naming-conventions.md](docs/cli/naming-conventions.md)
Expand Down
118 changes: 118 additions & 0 deletions bin/audit-oxlint-coverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {existsSync, readFileSync, writeFileSync} from 'node:fs'
import {matchesGlob, resolve} from 'node:path'
import {execFileSync} from 'node:child_process'

const root = execFileSync('git', ['rev-parse', '--show-toplevel'], {encoding: 'utf8'}).trim()
const read = (file) => JSON.parse(readFileSync(resolve(root, file), 'utf8'))
// Frozen effective configurations were captured from the original revision
// before removing ESLint. Auditing the migration must not reinstall its engine.
const baseline = read('configurations/oxlint-baseline.json')
const mapping = read('configurations/oxlint-rule-mapping.json')
const config = read('oxlint.json')
const scripts = read('package.json').scripts
const formattingInputs = [...scripts.prettier.matchAll(/"([^"]+)"/g)].map((match) => match[1])
const match = (file, patterns) => patterns?.some((pattern) => matchesGlob(file, pattern))
const severity = (value) => {
const level = Array.isArray(value) ? value[0] : value
return {off: 0, allow: 0, warn: 1, error: 2, deny: 2}[level] ?? level
}

const stable = (value) =>
JSON.stringify(value, (_, item) =>
item && typeof item === 'object' && !Array.isArray(item)
? Object.fromEntries(Object.entries(item).sort(([first], [second]) => first.localeCompare(second)))
: item,
)

const missing = new Map()
const options = new Set()
const severities = new Set()
const unsupported = new Set()
const supported = new Set()
const partial = new Set()
const missingFormatting = []
let filesCompared = 0
const retiredFiles = []
for (const group of baseline.configurations) {
const before = {...baseline.rules, ...group.rules}
for (const name of group.removed) delete before[name]
for (const file of group.files) {
if (!existsSync(resolve(root, file))) {
retiredFiles.push(file)
continue
}
const after = {...config.rules}
for (const override of config.overrides) {
if (match(file, override.files) && !match(file, override.excludeFiles)) Object.assign(after, override.rules)
}
// Preserve file-wide exemptions while comparing effective file scopes.
const header =
readFileSync(resolve(root, file), 'utf8').match(/^(?:\s*(?:\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/))*/)?.[0] ?? ''
for (const directive of header.matchAll(/eslint-disable(?!-)([^\n*]*)/g)) {
for (const name of directive[1]
.split(' -- ')[0]
.split(',')
.map((name) => name.trim()))
delete after[name]
}
if (!match(file, formattingInputs)) missingFormatting.push(file)
for (const [name, value] of Object.entries(before)) {
const replacement = mapping[name]
if (!replacement) throw new Error(`Undocumented original rule: ${name}`)
if (replacement.status === 'unsupported') {
unsupported.add(name)
continue
}
if (severity(after[replacement.replacement] ?? 'off') === 0) {
const examples = missing.get(name) ?? []
if (examples.length < 4) examples.push(file)
missing.set(name, examples)
continue
}
supported.add(name)
if (replacement.status === 'partial') partial.add(name)
if (severity(value) !== severity(after[replacement.replacement])) severities.add(name)
const parameters = (configuration) => (Array.isArray(configuration) ? configuration.slice(1) : [])
if (stable(parameters(value)) !== stable(parameters(after[replacement.replacement]))) options.add(name)
}
filesCompared++
}
}

// This supplementary inventory records verified tool overlap. It does not turn
// a missing dedicated rule into a claim of equivalent behavior or file scope.
const otherToolCoverage = Object.fromEntries(
[...unsupported]
.filter((name) => mapping[name].otherToolCoverage)
.map((name) => [name, mapping[name].otherToolCoverage]),
)

const summary = {
baselineCommit: baseline.baselineCommit,
filesCompared,
retiredFiles: retiredFiles.length,
baselineLintRules: Object.keys(mapping).length,
rulesWithReplacements: supported.size,
partialReplacements: partial.size,
rulesWithoutDedicatedReplacement: unsupported.size,
rulesWithOtherToolCoverage: Object.keys(otherToolCoverage).length,
rulesWithScopeGaps: missing.size,
optionDifferences: options.size,
severityDifferences: severities.size,
missingFormattingFiles: missingFormatting.length,
}
const findings = {
scopeGaps: Object.fromEntries(missing),
optionDifferences: [...options].sort(),
severityDifferences: [...severities].sort(),
rulesWithoutDedicatedReplacement: [...unsupported].sort(),
otherToolCoverage,
partialReplacements: [...partial].sort(),
missingFormatting,
}
const output = process.argv[2] ?? '/tmp/cli-oxlint-coverage-audit.json'
writeFileSync(output, JSON.stringify({summary, findings}, null, 2) + '\n')
console.log(JSON.stringify(summary, null, 2))
// Partial and unsupported replacements are deliberate and recorded in the
// mapping. Missing file scopes or formatting must be fixed or documented there.
if (missing.size || missingFormatting.length || severities.size) process.exitCode = 1
Loading
Loading