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
51 changes: 51 additions & 0 deletions bin/oxlint-fix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {createHash} from 'node:crypto'
import {globSync, readdirSync, readFileSync, writeFileSync} from 'node:fs'
import {dirname, join} from 'node:path'
import {createRequire} from 'node:module'
import {spawnSync} from 'node:child_process'

const require = createRequire(import.meta.url)
const oxlint = join(dirname(require.resolve('oxlint/package.json')), 'bin/oxlint')
const directories = process.argv.slice(2)
if (directories.length === 0) throw new Error('Provide at least one generated-code directory.')

// Group schema and scalar imports separately from the document-node import.
for (const directory of directories) {
for (const file of globSync('**/*.ts', {cwd: directory})) {
const filename = join(directory, file)
const source = readFileSync(filename, 'utf8')
const formatted = source.replace(
/^(import \* as Types from [^\n]+)\n\n(import \{JsonMapType\} from [^\n]+)\n/,
'$1\n$2\n\n',
)
if (formatted !== source) writeFileSync(filename, formatted)
}
}

function fingerprint(directory, hash) {
for (const entry of readdirSync(directory, {withFileTypes: true}).sort((first, second) => first.name.localeCompare(second.name))) {
const filename = join(directory, entry.name)
if (entry.isDirectory()) fingerprint(filename, hash)
else if (/\.tsx?$/.test(filename)) hash.update(filename).update(readFileSync(filename))
}
}

function currentFingerprint() {
const hash = createHash('sha256')
for (const directory of directories) fingerprint(directory, hash)
return hash.digest('hex')
}

// Import removal and ordering can produce overlapping fixes that need separate passes.
for (let attempt = 0; attempt < 10; attempt++) {
const before = currentFingerprint()
const result = spawnSync(process.execPath, [oxlint, '--config', '../../oxlint.json', '--fix', '--fix-suggestions', ...directories], {
encoding: 'utf8',
})
if (result.status === 0 || currentFingerprint() === before || attempt === 9) {
process.stdout.write(result.stdout ?? '')
process.stderr.write(result.stderr ?? '')
if (result.error) throw result.error
process.exit(result.status ?? 1)
}
}
26 changes: 26 additions & 0 deletions docs/cli/oxlint-rule-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Lint rule coverage

`pnpm lint` runs Prettier, Oxlint, and ESLint. Oxlint uses native rules and
JavaScript rules configured in `oxlint.json`.

The `compat` plugin in `packages/eslint-plugin-cli/oxlint-compat.js` runs shared
ESLint rules for module boundaries, import ordering and deprecations, naming and
member ordering, public API JSDoc, and comment conventions. The adapters supply
import-resolution settings and rule options. Naming checks use syntax-only
parser services; redundant type assertion checks use TypeScript parser services.

The lint script builds the Nx project graph before running Oxlint. Module
boundary checks require that graph. Generated GraphQL exemptions are configured
in `oxlint.json`.

Use `compat/<rule-name>` in disable directives for compatibility rules. The
adapter also accepts the upstream rule names. Oxlint checks unused directives.

The independent JavaScript rules in `packages/eslint-plugin-cli/oxlint.js` cover
naming, member and import ordering, workspace boundaries, unused imports, public
API documentation, and core/Node conventions. Their tests include an Oxlint run
that rejects loading ESLint or its plugins.

The public `@shopify/eslint-plugin-cli` package exports custom rules and a shared
ESLint configuration. Consumers of that configuration require ESLint and its
configured plugins.
7 changes: 7 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import nxPlugin from '@nx/eslint-plugin'
import compatibilityPlugin from './packages/eslint-plugin-cli/oxlint-compat.js'
import cliPlugin from '@shopify/eslint-plugin-cli'
import jsdocPlugin from 'eslint-plugin-jsdoc'
import {readFileSync} from 'node:fs'
Expand All @@ -12,6 +13,7 @@ const eslintPluginNames = {
}

const eslintRuleName = (oxlintRuleName) => {
if (oxlintRuleName.startsWith('compat/')) return compatibilityPlugin.ruleNames[oxlintRuleName.slice(7)]
const [pluginName, ...ruleNameParts] = oxlintRuleName.split('/')
const eslintPluginName = eslintPluginNames[pluginName]
return eslintPluginName ? `${eslintPluginName}/${ruleNameParts.join('/')}` : oxlintRuleName
Expand All @@ -34,6 +36,7 @@ const eslintCompatibilityConfig = ({files, rules, excludeFiles = []}) => ({

// Spread the CLI plugin's base config which includes all necessary plugins
export const eslintBaseConfig = [
{plugins: {compat: compatibilityPlugin}},
// Base config from @shopify/eslint-plugin-cli (includes shopify, typescript, prettier, etc.)
...cliPlugin.configs.config,

Expand Down Expand Up @@ -283,6 +286,10 @@ const config = [
},
eslintCompatibilityConfig(oxlintConfig),
...oxlintConfig.overrides.map((override) => eslintCompatibilityConfig(override)),
{
files: ['packages/app/src/cli/services/app-security-engine/embed-checks.mjs'],
rules: {'@shopify/strict-component-boundaries': 'off'},
},
]

export default config
25 changes: 18 additions & 7 deletions graphql.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ function projectFactory(name: string, schemaName: string, project: string = 'app
{'graphql-codegen-typescript-operation-types': {enumsAsTypes: true, useTypeImports: true}},
{
add: {
content:
"/* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any, tsdoc/syntax, @typescript-eslint/no-duplicate-type-constituents, @typescript-eslint/no-redundant-type-constituents, @nx/enforce-module-boundaries */\nimport {JsonMapType} from '@shopify/cli-kit/node/toml'",
content: "import {JsonMapType} from '@shopify/cli-kit/node/toml'",
},
},
],
Expand All @@ -34,8 +33,7 @@ function projectFactory(name: string, schemaName: string, project: string = 'app
plugins: [
{
add: {
content:
"/* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/naming-convention, @typescript-eslint/no-duplicate-type-constituents, @typescript-eslint/no-redundant-type-constituents, @nx/enforce-module-boundaries */\nimport {JsonMapType} from '@shopify/cli-kit/node/toml'",
content: "import {JsonMapType} from '@shopify/cli-kit/node/toml'",
},
},
{
Expand All @@ -44,6 +42,7 @@ function projectFactory(name: string, schemaName: string, project: string = 'app
skipTypename: true,
useTypeImports: true,
onlyOperationTypes: true,
defaultScalarType: 'unknown',
scalars: {
GlobalID: 'string',
PropertyId: 'string',
Expand Down Expand Up @@ -84,13 +83,25 @@ export default {
webhooks: projectFactory('webhooks', 'webhooks_schema.graphql'),
functions: projectFactory('functions', 'functions_cli_schema.graphql', 'app'),
adminAsApp: projectFactory('admin', 'admin_schema.graphql'),
organizationsDestinations: projectFactory('business-platform-destinations', 'destinations_schema.graphql', 'organizations'),
organizationsDestinations: projectFactory(
'business-platform-destinations',
'destinations_schema.graphql',
'organizations',
),
organizationsBusinessPlatformOrganizations: projectFactory(
'business-platform-organizations',
'organizations_schema.graphql',
'organizations',
),
storeBusinessPlatformDestinations: projectFactory('business-platform-destinations', 'destinations_schema.graphql', 'store'),
storeBusinessPlatformOrganizations: projectFactory('business-platform-organizations', 'organizations_schema.graphql', 'store'),
storeBusinessPlatformDestinations: projectFactory(
'business-platform-destinations',
'destinations_schema.graphql',
'store',
),
storeBusinessPlatformOrganizations: projectFactory(
'business-platform-organizations',
'organizations_schema.graphql',
'store',
),
},
}
Loading
Loading