@scayle/eslint-config-storefront 5.0.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +33 -1
- package/config/v3/importResolver.mjs +71 -0
- package/config/v3.mjs +20 -3
- package/config/v3.test.mjs +98 -0
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @aboutyou/eslint-config-sfc
|
|
2
2
|
|
|
3
|
+
## 5.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependency `eslint-plugin-unicorn@^73.0.0` to `eslint-plugin-unicorn@^74.0.0`
|
|
8
|
+
|
|
9
|
+
## 5.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- `@scayle/eslint-config-storefront/v3` now wires an `eslint-import-resolver-typescript` resolver by default, pointed at the project's `tsconfig.json`. Path aliases like `@client/*` or `@server/*` resolve to real files, so `import-x/no-restricted-paths` and `import-x/no-cycle` see through them instead of only matching literal relative imports.
|
|
14
|
+
|
|
15
|
+
Two rules also newly cover ground they didn't before, both enabled for the first time rather than tightened:
|
|
16
|
+
|
|
17
|
+
- `import-x/order` wasn't configured anywhere in `/v3` until now. It ships at `error`, with an explicit `groups` option ranking aliased imports alongside package imports, ahead of relative ones. A project already using `/v3` may see new ordering errors on upgrade.
|
|
18
|
+
- `import-x/no-cycle` previously only saw `.js`/`.mjs`/`.cjs` files. `.ts` and `.tsx` are now included, so a circular dependency that only existed between TypeScript files, alias or not, is caught for the first time.
|
|
19
|
+
|
|
20
|
+
`resolver.tsconfigPath` points the resolver at a specific tsconfig, independent of `typescript.tsconfigPath` (which also switches on type-aware TypeScript rules). Without either set, the resolver and the `no-restricted-paths` zones fall back to resolving relative to ESLint's working directory, correct when linting runs via `pnpm lint`/CI but not guaranteed for an editor's ESLint extension. Set `resolver.tsconfigPath` to an absolute path if that matters for your setup.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Added dependency `eslint-import-resolver-typescript@^4.4.5`
|
|
25
|
+
|
|
3
26
|
## 5.0.0
|
|
4
27
|
|
|
5
28
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ export default withNuxt(storefrontNuxt())
|
|
|
48
48
|
The Storefront Application V3 stack (Hono + Inertia.js + Vue 3, not Nuxt) should use the
|
|
49
49
|
`/v3` entry point. It builds on the default config and adds Vue language wiring, Vue rules,
|
|
50
50
|
accessibility rules, `vue-i18n` linting, `@scayle/vue-composable` rules, Storybook file
|
|
51
|
-
conventions,
|
|
51
|
+
conventions, Tailwind CSS class validation, and a TypeScript-aware import resolver:
|
|
52
52
|
|
|
53
53
|
```js
|
|
54
54
|
// eslint.config.mjs
|
|
@@ -67,10 +67,42 @@ export default storefrontV3({
|
|
|
67
67
|
`tailwind.entryPoint` and `i18n.localeDir` are required, the underlying plugins can't
|
|
68
68
|
validate Tailwind classes or i18n keys without a real path. Optional options:
|
|
69
69
|
|
|
70
|
+
- `typescript.tsconfigPath`: absolute path to a `tsconfig.json`. Enables type-aware
|
|
71
|
+
TypeScript rules and points the import resolver at that file instead of
|
|
72
|
+
`<root>/tsconfig.json`
|
|
73
|
+
- `resolver.tsconfigPath`: absolute path to a `tsconfig.json` for the import resolver only,
|
|
74
|
+
independent of `typescript.tsconfigPath`. Use this instead when you want the resolver
|
|
75
|
+
fix below without also switching on type-aware TypeScript rules
|
|
76
|
+
- `i18n.messageSyntaxVersion`: `vue-i18n` message syntax version, defaults to `^11.2.8`
|
|
70
77
|
- `i18n.unusedKeyIgnores`: regex-string patterns for translation keys used dynamically in code
|
|
71
78
|
- `vue.undefComponentIgnores`: custom element (web component) tag names to exempt from `vue/no-undef-components`
|
|
72
79
|
- `vueComposable.fallthroughAttrsIgnores`: path fragments to exempt from `require-explicit-fallthrough-attrs` (e.g. router-mounted page/layout roots that never receive fallthrough attributes)
|
|
73
80
|
|
|
81
|
+
#### Import resolution
|
|
82
|
+
|
|
83
|
+
`/v3` wires `eslint-import-resolver-typescript`, so `import-x` rules resolve `tsconfig.json`
|
|
84
|
+
path aliases (`@client/*`, `@server/*`, or any other `paths` entry) to their real files
|
|
85
|
+
instead of only matching literal relative imports. Three rules depend on that resolution
|
|
86
|
+
to work correctly against an aliased import:
|
|
87
|
+
|
|
88
|
+
- `import-x/no-restricted-paths`: zone violations across an alias fail lint, not just
|
|
89
|
+
across a relative path
|
|
90
|
+
- `import-x/no-cycle`: circular dependencies through an aliased import are caught. `.vue`
|
|
91
|
+
files are the one exception, a cycle through a `.vue` file only surfaces when ESLint
|
|
92
|
+
lints that file directly
|
|
93
|
+
- `import-x/order`: aliased imports rank alongside package imports (`external`), ahead of
|
|
94
|
+
relative ones, instead of dropping to last place once the resolver can see they resolve
|
|
95
|
+
to a project file
|
|
96
|
+
|
|
97
|
+
Without `resolver.tsconfigPath` or `typescript.tsconfigPath` set, both the resolver and
|
|
98
|
+
`import-x/no-restricted-paths`'s zones resolve relative to ESLint's own working directory.
|
|
99
|
+
That's correct for `pnpm lint` and CI, since a monorepo's task runner always runs a
|
|
100
|
+
package's lint script with that package as the working directory, but not guaranteed for
|
|
101
|
+
an editor's ESLint extension, which can run with the workspace root as its working
|
|
102
|
+
directory instead. Set `resolver.tsconfigPath` to an absolute path (for example,
|
|
103
|
+
`new URL('./tsconfig.json', import.meta.url).pathname` in your `eslint.config.mjs`) if your
|
|
104
|
+
editor's linting needs to match the command-line result.
|
|
105
|
+
|
|
74
106
|
### Add script for package.json
|
|
75
107
|
|
|
76
108
|
For example:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
|
2
|
+
|
|
3
|
+
// Resolves aliased imports (e.g. tsconfig.json `paths` like `@client/*`) to their real
|
|
4
|
+
// files, so import-x/no-restricted-paths, import-x/no-cycle, and import-x/order can see
|
|
5
|
+
// through them instead of only matching literal relative imports. Without this, every
|
|
6
|
+
// rule that compares resolved paths silently no-ops on an alias it can't resolve.
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} [tsconfigPath] Absolute path to the tsconfig.json to resolve aliases
|
|
9
|
+
* against (from `resolver.tsconfigPath`, falling back to `typescript.tsconfigPath`).
|
|
10
|
+
* Omit both to use the resolver's own `<cwd>/tsconfig.json` default.
|
|
11
|
+
* @returns {import('eslint').Linter.Config[]} Flat config entries wiring the resolver and
|
|
12
|
+
* the setting import-x/no-cycle needs to use it correctly.
|
|
13
|
+
*/
|
|
14
|
+
export function importResolverConfig(tsconfigPath) {
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
name: 'storefront/v3/import-resolver',
|
|
18
|
+
settings: {
|
|
19
|
+
'import-x/resolver-next': [
|
|
20
|
+
createTypeScriptImportResolver(
|
|
21
|
+
tsconfigPath ? { project: tsconfigPath } : undefined,
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
// import-x/no-cycle builds its own export map per file and gates that on
|
|
25
|
+
// this setting (distinct from the import-x/extensions *rule*). It defaults
|
|
26
|
+
// to .js/.mjs/.cjs only, so without this, every .ts/.tsx file silently
|
|
27
|
+
// returns a null export map and cycles through them go undetected past
|
|
28
|
+
// depth 1, alias or not.
|
|
29
|
+
//
|
|
30
|
+
// .vue is deliberately not listed here: the export map builder's parser
|
|
31
|
+
// (via import-x/parsers) can't replicate the <script setup lang="ts">
|
|
32
|
+
// sub-parser wiring vueConfig() sets up, so it errors on every .vue file's
|
|
33
|
+
// <template> block. Those errors are swallowed rather than surfaced as lint
|
|
34
|
+
// failures, but they also mean a walk that needs to build a .vue file's
|
|
35
|
+
// export map (rather than lint it directly) can't see past it. A cycle
|
|
36
|
+
// that runs through a .vue file still gets caught when ESLint lints that
|
|
37
|
+
// .vue file itself, since it parses that one file with the real language
|
|
38
|
+
// config, not the export map builder.
|
|
39
|
+
'import-x/extensions': ['.js', '.mjs', '.cjs', '.ts', '.tsx'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The default groups list omits 'internal' (the rank an aliased import gets once it
|
|
46
|
+
// resolves), which would otherwise drop every @client/@server/@shared-style import to
|
|
47
|
+
// last place instead of sorting with 'external' the way Vue 3 projects conventionally
|
|
48
|
+
// write them. 'builtin' stays its own rank, ahead of both: a `node:*` import belongs at
|
|
49
|
+
// the top of the file the same way it already does under the default groups.
|
|
50
|
+
/** @returns {import('eslint').Linter.Config[]} Flat config entries for import ordering, aware of resolved aliases. */
|
|
51
|
+
export function importOrderConfig() {
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
name: 'storefront/v3/import-order',
|
|
55
|
+
rules: {
|
|
56
|
+
'import-x/order': [
|
|
57
|
+
'error',
|
|
58
|
+
{
|
|
59
|
+
groups: [
|
|
60
|
+
'builtin',
|
|
61
|
+
['external', 'internal'],
|
|
62
|
+
'parent',
|
|
63
|
+
'sibling',
|
|
64
|
+
'index',
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
]
|
|
71
|
+
}
|
package/config/v3.mjs
CHANGED
|
@@ -5,11 +5,24 @@ import { vueI18nConfig } from './v3/vueI18n.mjs'
|
|
|
5
5
|
import { vueComposableConfig } from './v3/vueComposable.mjs'
|
|
6
6
|
import { storybookConfig } from './v3/storybook.mjs'
|
|
7
7
|
import { tailwindConfig } from './v3/tailwind.mjs'
|
|
8
|
+
import {
|
|
9
|
+
importResolverConfig,
|
|
10
|
+
importOrderConfig,
|
|
11
|
+
} from './v3/importResolver.mjs'
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* @typedef {object} EslintConfigStorefrontV3Options
|
|
11
15
|
* @property {object} [typescript] Options forwarded to `typescriptConfig` (see `storefront`).
|
|
12
16
|
* @property {string} [typescript.tsconfigPath] Enables type-aware TypeScript rules when set.
|
|
17
|
+
* @property {object} [resolver] Import resolver settings.
|
|
18
|
+
* @property {string} [resolver.tsconfigPath] Absolute path to the tsconfig.json the import
|
|
19
|
+
* resolver reads `paths` aliases from. Independent of `typescript.tsconfigPath`: passing
|
|
20
|
+
* an absolute path here does not switch on type-aware TypeScript rules. Omit to fall back
|
|
21
|
+
* to `typescript.tsconfigPath`, then to the resolver's own `<cwd>/tsconfig.json` default,
|
|
22
|
+
* which only resolves correctly when ESLint's working directory happens to be this project's
|
|
23
|
+
* root (true for `pnpm lint`/CI via Turborepo, not guaranteed for an editor's ESLint
|
|
24
|
+
* extension). Set this explicitly, as an absolute path anchored to the config file's own
|
|
25
|
+
* location, if ESLint might ever run with a different working directory.
|
|
13
26
|
* @property {object} tailwind Tailwind CSS settings.
|
|
14
27
|
* @property {string} tailwind.entryPoint Path to the project's Tailwind CSS entry point.
|
|
15
28
|
* @property {object} i18n `vue-i18n` linting settings.
|
|
@@ -26,14 +39,16 @@ import { tailwindConfig } from './v3/tailwind.mjs'
|
|
|
26
39
|
* Builds the SCAYLE Storefront ESLint flat config for the Storefront Application V3 stack
|
|
27
40
|
* (Hono + Inertia.js + Vue 3, not Nuxt). Extends `storefront()` with Vue language wiring,
|
|
28
41
|
* Vue rules, accessibility rules, `vue-i18n` rules, `@scayle/vue-composable` rules,
|
|
29
|
-
* Storybook file conventions,
|
|
30
|
-
* `storefront()`/`storefrontNuxt()` since those two
|
|
42
|
+
* Storybook file conventions, Tailwind CSS class validation, and a TypeScript-aware
|
|
43
|
+
* import resolver. Kept separate from `storefront()`/`storefrontNuxt()` since those two
|
|
44
|
+
* stay framework-agnostic.
|
|
31
45
|
*
|
|
32
46
|
* @param {EslintConfigStorefrontV3Options} config
|
|
33
47
|
* @returns {FlatConfigComposer<import('eslint').Linter.Config, never>} Returns a FlatConfigComposer object to be consumed in projects eslint.config.mjs files
|
|
34
48
|
*/
|
|
35
49
|
export const storefrontV3 = function (config) {
|
|
36
|
-
const { typescript, tailwind, i18n, vue, vueComposable } =
|
|
50
|
+
const { typescript, resolver, tailwind, i18n, vue, vueComposable } =
|
|
51
|
+
config ?? {}
|
|
37
52
|
|
|
38
53
|
/** @type {FlatConfigComposer<import('eslint').Linter.Config, never>} */
|
|
39
54
|
const flatConfigComposer = new FlatConfigComposer()
|
|
@@ -45,5 +60,7 @@ export const storefrontV3 = function (config) {
|
|
|
45
60
|
...vueConfig(vue),
|
|
46
61
|
...vueI18nConfig(i18n),
|
|
47
62
|
...vueComposableConfig(vueComposable),
|
|
63
|
+
...importResolverConfig(resolver?.tsconfigPath ?? typescript?.tsconfigPath),
|
|
64
|
+
...importOrderConfig(),
|
|
48
65
|
)
|
|
49
66
|
}
|
package/config/v3.test.mjs
CHANGED
|
@@ -27,4 +27,102 @@ describe('storefrontV3', () => {
|
|
|
27
27
|
expect(Array.isArray(resolved)).toBe(true)
|
|
28
28
|
expect(resolved.length).toBeGreaterThan(0)
|
|
29
29
|
})
|
|
30
|
+
|
|
31
|
+
// These settings have no test-visible failure mode of their own: a reverted or
|
|
32
|
+
// misconfigured resolver/extensions/groups entry still produces a config that loads
|
|
33
|
+
// and lints without error, it just silently stops catching what it's meant to catch.
|
|
34
|
+
// Asserting the shape here is what makes a quiet revert fail CI instead of passing it.
|
|
35
|
+
it('wires the import resolver and no-cycle extensions', async () => {
|
|
36
|
+
const config = storefrontV3({
|
|
37
|
+
tailwind: { entryPoint: './src/client/index.css' },
|
|
38
|
+
i18n: { localeDir: './src/i18n/locales/*.json' },
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const resolved = await config.toConfigs()
|
|
42
|
+
const resolverEntry = resolved.find(
|
|
43
|
+
(c) => c.name === 'storefront/v3/import-resolver',
|
|
44
|
+
)
|
|
45
|
+
if (!resolverEntry) {
|
|
46
|
+
throw new Error('resolverEntry not found')
|
|
47
|
+
}
|
|
48
|
+
const { settings } = resolverEntry
|
|
49
|
+
if (!settings) {
|
|
50
|
+
throw new Error('resolverEntry.settings not found')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const resolverNext =
|
|
54
|
+
/** @type {{ name: string, interfaceVersion: number }[]} */ (
|
|
55
|
+
settings['import-x/resolver-next']
|
|
56
|
+
)
|
|
57
|
+
expect(resolverNext).toHaveLength(1)
|
|
58
|
+
expect(resolverNext[0]).toMatchObject({
|
|
59
|
+
name: 'eslint-import-resolver-typescript',
|
|
60
|
+
interfaceVersion: 3,
|
|
61
|
+
})
|
|
62
|
+
expect(settings['import-x/extensions']).toEqual([
|
|
63
|
+
'.js',
|
|
64
|
+
'.mjs',
|
|
65
|
+
'.cjs',
|
|
66
|
+
'.ts',
|
|
67
|
+
'.tsx',
|
|
68
|
+
])
|
|
69
|
+
expect(settings['import-x/extensions']).not.toContain('.vue')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('ranks internal imports alongside external ones in import-x/order', async () => {
|
|
73
|
+
const config = storefrontV3({
|
|
74
|
+
tailwind: { entryPoint: './src/client/index.css' },
|
|
75
|
+
i18n: { localeDir: './src/i18n/locales/*.json' },
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const resolved = await config.toConfigs()
|
|
79
|
+
const orderEntry = resolved.find(
|
|
80
|
+
(c) => c.name === 'storefront/v3/import-order',
|
|
81
|
+
)
|
|
82
|
+
if (!orderEntry) {
|
|
83
|
+
throw new Error('orderEntry not found')
|
|
84
|
+
}
|
|
85
|
+
const { rules } = orderEntry
|
|
86
|
+
if (!rules) {
|
|
87
|
+
throw new Error('orderEntry.rules not found')
|
|
88
|
+
}
|
|
89
|
+
const orderRule = rules['import-x/order']
|
|
90
|
+
if (!orderRule) {
|
|
91
|
+
throw new Error('import-x/order rule not found')
|
|
92
|
+
}
|
|
93
|
+
const [severity, options] = /** @type {[string, { groups: unknown[] }]} */ (
|
|
94
|
+
orderRule
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
expect(severity).toBe('error')
|
|
98
|
+
expect(options.groups).toEqual([
|
|
99
|
+
'builtin',
|
|
100
|
+
['external', 'internal'],
|
|
101
|
+
'parent',
|
|
102
|
+
'sibling',
|
|
103
|
+
'index',
|
|
104
|
+
])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('accepts resolver.tsconfigPath independently of typescript.tsconfigPath', async () => {
|
|
108
|
+
// Both must resolve without throwing: resolver.tsconfigPath alone (no type-aware
|
|
109
|
+
// TypeScript rules switched on) and typescript.tsconfigPath alone (the fallback
|
|
110
|
+
// path, still feeding the resolver). The resolver's own internals aren't asserted
|
|
111
|
+
// on here, only that both option shapes are accepted.
|
|
112
|
+
await expect(
|
|
113
|
+
storefrontV3({
|
|
114
|
+
tailwind: { entryPoint: './src/client/index.css' },
|
|
115
|
+
i18n: { localeDir: './src/i18n/locales/*.json' },
|
|
116
|
+
resolver: { tsconfigPath: '/absolute/resolver-tsconfig.json' },
|
|
117
|
+
}).toConfigs(),
|
|
118
|
+
).resolves.toBeInstanceOf(Array)
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
storefrontV3({
|
|
122
|
+
tailwind: { entryPoint: './src/client/index.css' },
|
|
123
|
+
i18n: { localeDir: './src/i18n/locales/*.json' },
|
|
124
|
+
typescript: { tsconfigPath: '/absolute/typescript-tsconfig.json' },
|
|
125
|
+
}).toConfigs(),
|
|
126
|
+
).resolves.toBeInstanceOf(Array)
|
|
127
|
+
})
|
|
30
128
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scayle/eslint-config-storefront",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"description": "SCAYLE Commerce Engine specific eslint configuration",
|
|
5
5
|
"author": "SCAYLE Commerce Engine",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,9 +41,11 @@
|
|
|
41
41
|
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
|
|
42
42
|
"@eslint/js": "^10.0.0",
|
|
43
43
|
"@intlify/eslint-plugin-vue-i18n": "^4.5.1",
|
|
44
|
+
"@scayle/eslint-plugin-vue-composable": "1.2.1",
|
|
44
45
|
"@vitest/eslint-plugin": "^1.6.20",
|
|
45
46
|
"eslint-config-flat-gitignore": "^2.3.0",
|
|
46
47
|
"eslint-flat-config-utils": "^3.2.0",
|
|
48
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
47
49
|
"eslint-plugin-better-tailwindcss": "^4.7.0",
|
|
48
50
|
"eslint-plugin-import-x": "^4.17.1",
|
|
49
51
|
"eslint-plugin-jsdoc": "^64.2.1",
|
|
@@ -54,20 +56,19 @@
|
|
|
54
56
|
"eslint-plugin-regexp": "^3.2.0",
|
|
55
57
|
"eslint-plugin-sonarjs": "^4.2.0",
|
|
56
58
|
"eslint-plugin-storybook": "^10.5.8",
|
|
57
|
-
"eslint-plugin-unicorn": "^
|
|
59
|
+
"eslint-plugin-unicorn": "^74.0.0",
|
|
58
60
|
"eslint-plugin-vue": "^10.10.0",
|
|
59
61
|
"eslint-plugin-vuejs-accessibility": "^2.6.0",
|
|
60
62
|
"globals": "^17.7.0",
|
|
61
63
|
"typescript-eslint": "^8.67.0",
|
|
62
|
-
"vue-eslint-parser": "^10.4.1"
|
|
63
|
-
"@scayle/eslint-plugin-vue-composable": "1.2.1"
|
|
64
|
+
"vue-eslint-parser": "^10.4.1"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"@types/node": "^24",
|
|
67
68
|
"@vitest/coverage-v8": "4.1.11",
|
|
68
|
-
"eslint": "10.
|
|
69
|
+
"eslint": "10.9.1",
|
|
69
70
|
"eslint-formatter-gitlab": "7.2.0",
|
|
70
|
-
"publint": "0.3.
|
|
71
|
+
"publint": "0.3.24",
|
|
71
72
|
"typescript": "6.0.3",
|
|
72
73
|
"vitest": "4.1.11"
|
|
73
74
|
},
|