@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 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, and Tailwind CSS class validation:
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, and Tailwind CSS class validation. Kept separate from
30
- * `storefront()`/`storefrontNuxt()` since those two stay framework-agnostic.
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 } = config ?? {}
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
  }
@@ -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.0.0",
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": "^73.0.0",
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.8.1",
69
+ "eslint": "10.9.1",
69
70
  "eslint-formatter-gitlab": "7.2.0",
70
- "publint": "0.3.23",
71
+ "publint": "0.3.24",
71
72
  "typescript": "6.0.3",
72
73
  "vitest": "4.1.11"
73
74
  },