@scayle/eslint-config-storefront

SCAYLE Commerce Engine specific eslint configuration
218,314
Install
npm install @scayle/eslint-config-storefront
Latest Version:5.3.1
Vue:3
License:MIT
Last Updated:Oct 19, 2026
Links: npm
Maintainer: scayle-automation

@scayle/eslint-config-storefront

A shared eslint config for SCAYLE Storefront projects and packages. Provides SCAYLE-specific eslint configuration for Frontend and Node.js projects using TypeScript.

Flat config only (ESLint 10.4+). Built directly on first-class plugins (@eslint/js, typescript-eslint, eslint-plugin-n, eslint-plugin-jsonc, eslint-plugin-jsdoc, eslint-plugin-unicorn, eslint-plugin-regexp, @e18e/eslint-plugin, and others) with no shared-config aggregator dependency.

Should you require support for eslint@8, please use @scayle/eslint-config-storefront@3.


Entry points

The package exposes three entry points, all built on the same shared rule set. Pick the one that matches your stack:

Entry point Export For Adds on top of the shared rules
Default @scayle/eslint-config-storefront Framework-agnostic TypeScript/Node.js packages JavaScript, TypeScript, JSDoc, and import rules
Nuxt @scayle/eslint-config-storefront/nuxt Nuxt projects — this is what Storefront Application V2 uses, since V2 is a Nuxt 4 application Nothing beyond the shared rules; @nuxt/eslint already supplies JavaScript, TypeScript, JSDoc, and import rules
V3 @scayle/eslint-config-storefront/v3 The Storefront Application V3 stack (Hono + Inertia.js + Vue 3, not Nuxt) Everything the default entry point adds, plus Vue language wiring, Vue rules, accessibility rules, vue-i18n linting, @scayle/vue-composable rules, Storybook file conventions, Tailwind CSS class validation, a TypeScript-aware import resolver, and the require-lazy-promise-in-view-props rule

There is no /v2 entry point: Storefront Application V2 is a Nuxt application, so it uses the /nuxt entry point directly, the same as any other Nuxt project. "V2" and "V3" describe the two Storefront Application stacks this monorepo ships, not two separate ESLint configs — V2 maps to /nuxt, V3 maps to /v3.

Internally, /nuxt and /v3 both build on the same base config, so a rule fixed or added to the shared set (JavaScript, Node.js, JSON/JSONC, test files, SonarJS, Promise usage, modernization, regex safety, comments hygiene) applies to every entry point without duplicating configuration. Only /v3 and the default entry point add TypeScript, JSDoc, and import rules directly — /nuxt relies on @nuxt/eslint for those instead.

Install

Add the @scayle/eslint-config-storefront manually to your project:

pnpm i -D eslint @scayle/eslint-config-storefront

yarn add -D eslint @scayle/eslint-config-storefront

And create a eslint.config.mjs in your project root:

// eslint.config.mjs
import storefront from '@scayle/eslint-config-storefront'

export default storefront()

Nuxt projects (Storefront Application V2)

Nuxt projects already get JavaScript, TypeScript, JSDoc, and import rules from @nuxt/eslint. Use the /nuxt entry point instead, which only adds the framework-agnostic rules (comments hygiene, Node.js, JSON/JSONC, test-file conventions, SonarJS, Promise usage, modernization, and regex safety):

// eslint.config.mjs
import storefrontNuxt from '@scayle/eslint-config-storefront/nuxt'
import withNuxt from './.nuxt/eslint.config.mjs'

export default withNuxt(storefrontNuxt())

This is the entry point Storefront Application V2's own eslint.config.mjs uses.

Storefront Application V3 projects

The Storefront Application V3 stack (Hono + Inertia.js + Vue 3, not Nuxt) should use the /v3 entry point. It builds on the default config and adds Vue language wiring, Vue rules, accessibility rules, vue-i18n linting, @scayle/vue-composable rules, Storybook file conventions, Tailwind CSS class validation, a TypeScript-aware import resolver, and the require-lazy-promise-in-view-props rule:

// eslint.config.mjs
import storefrontV3 from '@scayle/eslint-config-storefront/v3'

export default storefrontV3({
  tailwind: {
    entryPoint: './src/client/index.css',
  },
  i18n: {
    localeDir: './src/i18n/locales/*.json',
  },
})

tailwind.entryPoint and i18n.localeDir are required, the underlying plugins can't validate Tailwind classes or i18n keys without a real path. Optional options:

  • typescript.tsconfigPath: absolute path to a tsconfig.json. Enables type-aware TypeScript rules and points the import resolver at that file instead of <root>/tsconfig.json
  • resolver.tsconfigPath: absolute path to a tsconfig.json for the import resolver and, with the same ?? typescript.tsconfigPath fallback, for type-checking the require-lazy-promise-in-view-props rule below — independent of typescript.tsconfigPath in the sense that setting it does not switch on @typescript-eslint/* type-checked rules (recommendedTypeChecked) generally. Use this instead of typescript.tsconfigPath when you want the resolver fix and the lazy-promise rule without also switching on @typescript-eslint/* type-checked rules project-wide. When neither option resolves a tsconfig path, require-lazy-promise-in-view-props is omitted entirely rather than registered without type information. Setting it DOES have one other real effect: because ESLint flat config merges type information across every config block matching a file, and the lazy-promise rule's own block matches every .ts/.tsx/.mts/.cts file (not just files with a getViewProps method), eslint-plugin-sonarjs's type-aware rules gain real type information project-wide too — they silently report nothing without a type checker, so expect new sonarjs findings the first time you set this option, not just wherever the lazy-promise rule itself looks
  • i18n.messageSyntaxVersion: vue-i18n message syntax version, defaults to ^11.2.8
  • i18n.unusedKeyIgnores: regex-string patterns for translation keys used dynamically in code
  • vue.undefComponentIgnores: custom element (web component) tag names to exempt from vue/no-undef-components
  • vueComposable.fallthroughAttrsIgnores: path fragments to exempt from require-explicit-fallthrough-attrs (e.g. router-mounted page/layout roots that never receive fallthrough attributes)

This is the entry point Storefront Application V3's own eslint.config.mjs uses.

Vue rules and accessibility

/v3 wires the Vue parser (including <script setup lang="ts">) via vue-eslint-parser, applies eslint-plugin-vue's essential, strongly-recommended, and recommended presets, and layers a handful of Storefront-specific overrides on top (e.g. vue/custom-event-name-casing, vue/block-order, styling rules disabled where they conflict with this project's Prettier config). vue-eslint-parser doesn't reuse the type-aware @typescript-eslint/no-unused-vars config from the TypeScript rules, since that one only scopes to .ts/.tsx/.mts/.cts files, so .vue files get their own registration of the same rule.

Vue accessibility rules (eslint-plugin-vuejs-accessibility's recommended preset) are wired but every rule ships off by default. That baseline reflects that a11y coverage is opt-in until a consuming project runs its own audit and turns rules on deliberately — it is not a statement that accessibility doesn't matter.

vue-i18n linting

Validates message syntax, catches missing/unused/duplicate translation keys, and — only in CI (process.env.CI set) — enforces that every locale file carries the same keys as the others at error (a local warn lets you author a key in one locale before running the translation step). Requires i18n.localeDir to be set; there is no default, since the plugin can't validate anything without a real path to your locale files.

@scayle/vue-composable rules

Enables all five rules from the sibling @scayle/eslint-plugin-vue-composable package at error: no-composable-after-await, no-lifecycle-after-await, no-watch-after-await, no-computed-after-await (composables/lifecycle hooks/watchers/computed refs called after an await inside a composable bind to the wrong instance), and require-explicit-fallthrough-attrs (multi-root components must declare inheritAttrs: false, since Vue can't infer where fallthrough attributes should land). See that package's own README for the full rationale and examples per rule.

Storybook conventions

Applies eslint-plugin-storybook's recommended preset to files matching *.stories.* / *.story.*. No configuration needed.

Tailwind CSS class validation

Validates Tailwind utility classes via eslint-plugin-better-tailwindcss's recommended preset, against the CSS entry point you provide in tailwind.entryPoint. Class-order and line-wrapping rules are disabled, since this project's Prettier Tailwind plugin already handles sorting and would otherwise conflict with the ESLint plugin's own ordering opinion. no-deprecated-classes is disabled too: the /v3 entry point targets Tailwind v4, so this rule's v3 class-name findings don't apply.

Import resolution

/v3 wires eslint-import-resolver-typescript, so import-x rules resolve tsconfig.json path aliases (@client/*, @server/*, or any other paths entry) to their real files instead of only matching literal relative imports. Three rules depend on that resolution to work correctly against an aliased import:

  • import-x/no-restricted-paths: zone violations across an alias fail lint, not just across a relative path
  • import-x/no-cycle: circular dependencies through an aliased import are caught. .vue files are the one exception, a cycle through a .vue file only surfaces when ESLint lints that file directly
  • import-x/order: aliased imports rank alongside package imports (external), ahead of relative ones, instead of dropping to last place once the resolver can see they resolve to a project file

Without resolver.tsconfigPath or typescript.tsconfigPath set, both the resolver and import-x/no-restricted-paths's zones resolve relative to ESLint's own working directory. That's correct for pnpm lint and CI, since a monorepo's task runner always runs a package's lint script with that package as the working directory, but not guaranteed for an editor's ESLint extension, which can run with the workspace root as its working directory instead. Set resolver.tsconfigPath to an absolute path (for example, new URL('./tsconfig.json', import.meta.url).pathname in your eslint.config.mjs) if your editor's linting needs to match the command-line result.

require-lazy-promise-in-view-props

Enforces the lazyPromise convention in Inertia controllers: a promise created inside a controller's getViewProps() method must be wrapped in the SDK's lazyPromise(() => ...) helper, unless it's awaited or returned in the same statement it's created in. This closes two related problems that a promise created eagerly in getViewProps() can cause: a partial reload that excludes every prop consuming that promise still triggers the fetch (wasted work), and an uncaught rejection on such a promise can surface as an unhandled promise rejection. The rule reports at error and offers a suggestion (not an autofix, since call sites also need to change from a bare reference to an accessor call, x → x()) that wraps the flagged expression.

// Before — flagged
const productsPromise = fetchProducts(params)
return {
  products: async () => (await productsPromise).items,
  pagination: async () => (await productsPromise).pagination,
}

// After — the suggested fix, plus updating both call sites
const getProducts = lazyPromise(() => fetchProducts(params))
return {
  products: async () => (await getProducts()).items,
  pagination: async () => (await getProducts()).pagination,
}

The rule is type-aware: it detects any thenable-typed local (not just a literal Promise name match), so a PromiseLike-shaped or branded return type is caught too. It only checks locals declared directly in getViewProps()'s own body — a promise created and consumed entirely inside a nested function (a prop callback, a nested class's method) is not flagged, since that promise is already effectively lazy: the enclosing function only runs when it's invoked. A correctly-wrapped lazyPromise(() => ...) call itself returns a function, not a thenable, so it never trips the check; passing lazyPromise an already-invoked promise instead of a factory (lazyPromise(fetchThing()) instead of lazyPromise(() => fetchThing())) is a type error the TypeScript compiler catches on its own — the wrong argument shape doesn't satisfy lazyPromise's signature — so the rule doesn't need its own check for that case.

Known limitations: the rule does not verify that the lazyPromise identifier it sees resolves to the SDK's own export — a same-named local declaration would also satisfy it. It likewise doesn't verify that a consuming prop callback actually chains the accessor's result into its own return value; a callback that calls the accessor without awaiting or returning it could still produce an unhandled rejection the rule won't catch. It also doesn't track references across sibling declarators in the same body, so two independent promises consumed together via Promise.all([a, b]) are each flagged individually even though neither needs lazyPromise there.

Requires resolver.tsconfigPath or typescript.tsconfigPath to be set — see Import resolution above. When neither is set, the rule is omitted entirely from the config rather than registered without type information, which would otherwise crash ESLint on the first .ts file it lints. Note that resolver.tsconfigPath is passed to parserOptions.projectService.defaultProject, which is a fallback for files not covered by an auto-discovered tsconfig.json — and which typescript-eslint hard-fails on after 8 unmatched files by design. If your project's own tsconfig.json doesn't cover every .ts file this rule lints, you may see an ESLint crash rather than a lint error.

Add script for package.json

For example:

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}

VS Code support (auto fix on save)

Install VS Code ESLint extension

Add the following settings to your .vscode/settings.json:

{
  // Enable the ESlint flat config support
  "eslint.experimental.useFlatConfig": true,

  // Auto fix
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "never",
  },

  // Enable eslint for all supported languages
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue",
    "html",
    "markdown",
    "json",
    "jsonc",
    "yaml",
  ],
}

What the shared rules cover

Every entry point (default, /nuxt, /v3) includes the following rule domains. They live in config/rules.mjs, config/javascript.mjs, config/typescript.mjs, config/unicorn.mjs, config/test.mjs, and config/ignores.mjs in this package's source, one factory function per domain:

  • Comments hygiene (eslint-comments/*): no aggregating/duplicate/unlimited eslint-disable comments, no unused eslint-enable.
  • Node.js (node/*): deprecated API usage, callback error-handling conventions, process/Buffer globals preferred over the bare identifiers.
  • JSON/JSONC (jsonc/*): structural validity plus formatting rules for .json, .json5, and .jsonc files.
  • Test files (vitest/*, no-only-tests/*): it/describe conventions, no duplicate test titles, no accidental .only left in committed code (error in CI, warn locally so you can use .only while debugging).
  • SonarJS (sonarjs/*): the plugin's recommended preset, with no-duplicate-string off, todo-tag/fixme-tag downgraded to warn, and aws-restricted-ip-admin-access off (it only fires on AWS CDK infrastructure code).
  • Promise usage (promise/*): a curated subset of eslint-plugin-promise's rules — no-multiple-resolved, no-new-statics, and no-return-wrap at error, the rest at warn. catch-or-return and always-return only inspect a promise used as a bare statement, so a project whose promises are all stored in a variable before being awaited or returned (as V3's controller code is) can safely turn both off in its own eslint.config.mjs — Storefront Application V3's own config does exactly that. (require-lazy-promise-in-view-props is what enforces the equivalent convention for V3 controllers; see above.)
  • Modernization (unicorn/*, e18e/*): a curated subset of eslint-plugin-unicorn (not its full recommended preset, which is hundreds of largely stylistic rules), plus the e18e community's rule set for modernization, module-replacement, and performance findings. Every e18e/* rule ships at warn — the plugin is still pre-1.0 and its rule set hasn't been reviewed against every consuming project yet, so a warning surfaces a finding without failing your build. See e18e/* rules ship as warnings below for the one rule worth knowing about specifically.
  • Regex safety (regexp/*): eslint-plugin-regexp's recommended preset, including ReDoS-prone pattern detection.

The default and /v3 entry points additionally include:

  • JavaScript (@eslint/js's recommended preset plus Storefront-specific additions): strict equality, no implicit globals, consistent const/let usage, and similar correctness rules that apply regardless of TypeScript.
  • TypeScript (@typescript-eslint/*): the plugin's recommended preset, upgraded to recommendedTypeChecked when a tsconfigPath is provided (see Storefront Application V3 projects above for how V3 supplies one), plus Storefront-specific overrides — consistent type-only imports, interface over type for object shapes, unused-variable detection that allows a leading underscore to mark an intentionally-unused binding.
  • JSDoc (jsdoc/*): validates JSDoc comment structure (parameter names, return descriptions, alignment) at warn. This package is published and consumed by external developers, so exported symbols are expected to carry meaningful documentation.
  • Import correctness (import-x/*): duplicate imports, mutable exports, self-imports, extraneous dependencies not declared in package.json, and consistent type-import specifier style.
  • Ignore patterns: build artifacts, lockfiles, and tooling caches (node_modules, dist, .output, coverage reports, and similar) are excluded, on top of whatever your project's own .gitignore excludes.

/nuxt omits all five of the above: @nuxt/eslint already provides JavaScript, TypeScript, JSDoc, and import rules for a Nuxt project, and Nuxt's own tooling manages its build-output ignore patterns.

Customization

Default config / @scayle/eslint-config-storefront

// eslint.config.mjs
import storefront from '@scayle/eslint-config-storefront'

export default storefront({
  typescript: {
    // Enables type-aware rules for the given tsconfig
    tsconfigPath: 'path/to/tsconfig.json',
  },
})

Nuxt config / @scayle/eslint-config-storefront/nuxt

// eslint.config.mjs
import storefrontNuxt from '@scayle/eslint-config-storefront/nuxt'

export default storefrontNuxt()

Storefront Application V3 config / @scayle/eslint-config-storefront/v3

See Storefront Application V3 projects above for the full option list.

All three entry points return a FlatConfigComposer, so you can chain .append(...), .prepend(...), .override(...), or .renamePlugins(...) to layer project-specific rules on top.

Plugin names

Rules use each plugin's own published name (@typescript-eslint/*, import-x/*, jsonc/*, jsdoc/*, node/*, promise/*, sonarjs/*, unicorn/*, regexp/*, e18e/*, vitest/*, eslint-comments/*). There's no alias renaming. When you override or disable a rule inline, use that plugin's real prefix:

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type Foo = { bar: 2 }

e18e/* rules ship as warnings

The e18e community's rule set (modernization, module-replacement, and performance rules) is enabled by default, but every e18e/* rule ships at warn, not error. The plugin is still pre-1.0, and its rule set hasn't been reviewed against every consuming project. A warning won't fail your build.

One rule worth knowing about specifically: e18e/ban-dependencies flags a dependency in your project that has a lighter recommended alternative (for example ora in favor of nanospinner). Treat that finding as something to evaluate, not something to fix immediately. Swapping a dependency is a real decision with its own testing cost, not a lint fix. If you've decided to keep a flagged dependency, allowlist it explicitly rather than turning the rule off:

export default storefront().append({
  rules: {
    'e18e/ban-dependencies': ['warn', { allowed: ['ora'] }],
  },
})

View what rules are enabled

@eslint/config-inspector is a visual tool maintained by the ESLint core team to help you view what rules are enabled in your project and which files they apply to.

Go into your project root that contains eslint.config.js and run:

npx @eslint/config-inspector

License

Licensed under the MIT License

Related Packages

@forsakringskassan/eslint-config-vue

Försäkringskassans eslint shareable vue config

286,740 0
eslint-config-vue

eslint config for vue.js projects

313,105 320
@cloudflight/eslint-plugin-vue

Cloudflight eslint-plugin & eslint-config for vue

38,060
@html-validate/eslint-config-vue

Eslint sharable config used by the various HTML-validate packages

24,074

Version History

Version Released License
5.3.1 MIT
5.3.0 MIT
5.2.2 MIT
5.2.1 MIT
5.2.0 MIT
Pre-releases (1)
Version Released License
4.8.3-alpha.1 MIT