@gem-sdk/eslint-plugin 0.0.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/README.md +298 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/lib/moduleInfo.d.ts +27 -0
- package/dist/lib/moduleInfo.d.ts.map +1 -0
- package/dist/lib/moduleInfo.js +24 -0
- package/dist/rules/moduleImportBoundary.d.ts +4 -0
- package/dist/rules/moduleImportBoundary.d.ts.map +1 -0
- package/dist/rules/moduleImportBoundary.js +84 -0
- package/dist/rules/moduleIndexRequired.d.ts +10 -0
- package/dist/rules/moduleIndexRequired.d.ts.map +1 -0
- package/dist/rules/moduleIndexRequired.js +49 -0
- package/dist/rules/moduleStructure.d.ts +4 -0
- package/dist/rules/moduleStructure.d.ts.map +1 -0
- package/dist/rules/moduleStructure.js +74 -0
- package/dist/rules/noExportTypeFromTsx.d.ts +8 -0
- package/dist/rules/noExportTypeFromTsx.d.ts.map +1 -0
- package/dist/rules/noExportTypeFromTsx.js +29 -0
- package/dist/rules/noImportFromPackagePath.d.ts +4 -0
- package/dist/rules/noImportFromPackagePath.d.ts.map +1 -0
- package/dist/rules/noImportFromPackagePath.js +41 -0
- package/dist/rules/noModuleBarrel.d.ts +4 -0
- package/dist/rules/noModuleBarrel.d.ts.map +1 -0
- package/dist/rules/noModuleBarrel.js +32 -0
- package/dist/rules/noTForVariable.d.ts +4 -0
- package/dist/rules/noTForVariable.d.ts.map +1 -0
- package/dist/rules/noTForVariable.js +57 -0
- package/dist/rules/notUseCustomColorClass.d.ts +4 -0
- package/dist/rules/notUseCustomColorClass.d.ts.map +1 -0
- package/dist/rules/notUseCustomColorClass.js +82 -0
- package/dist/rules/notUseStoreToRefs.d.ts +7 -0
- package/dist/rules/notUseStoreToRefs.d.ts.map +1 -0
- package/dist/rules/notUseStoreToRefs.js +29 -0
- package/dist/rules/pureUiLayer.d.ts +4 -0
- package/dist/rules/pureUiLayer.d.ts.map +1 -0
- package/dist/rules/pureUiLayer.js +66 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# eslint-plugin-gem-fe
|
|
2
|
+
|
|
3
|
+
Shared ESLint rules for all GemPages frontend repositories.
|
|
4
|
+
|
|
5
|
+
Consolidates every custom rule from `web-builder-shopify-app`, `web-builder`, and `web-builder-elements` into a single publishable npm package. The architecture rules support both React (`.tsx`) and Vue (`.vue`) files.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
yarn add -D eslint-plugin-gem-fe
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Choose the config that matches your tech stack:
|
|
16
|
+
|
|
17
|
+
### React projects
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"extends": ["plugin:gem-fe/react"]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Vue projects
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"extends": ["plugin:gem-fe/vue"]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### All repos (architecture + i18n only)
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"extends": ["plugin:gem-fe/recommended"]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Manual rule selection
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"plugins": ["gem-fe"],
|
|
46
|
+
"rules": {
|
|
47
|
+
"gem-fe/module-structure": "error",
|
|
48
|
+
"gem-fe/no-module-barrel": "error",
|
|
49
|
+
"gem-fe/module-import-boundary": "error",
|
|
50
|
+
"gem-fe/pure-ui-layer": "error",
|
|
51
|
+
"gem-fe/module-index-required": "error",
|
|
52
|
+
"gem-fe/no-t-for-variable": "error",
|
|
53
|
+
"gem-fe/not-use-store-to-refs": "error",
|
|
54
|
+
"gem-fe/not-use-custom-color-class": "warn",
|
|
55
|
+
"gem-fe/no-export-type-from-tsx": "error",
|
|
56
|
+
"gem-fe/no-import-from-package-path": "error"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Rules
|
|
64
|
+
|
|
65
|
+
### Architecture (React + Vue)
|
|
66
|
+
|
|
67
|
+
These rules apply to both `.tsx` and `.vue` files and enforce the v2 module structure used across all GemPages repos.
|
|
68
|
+
|
|
69
|
+
Module detection handles both project layouts:
|
|
70
|
+
- `v2/modules/<name>/` — business modules (code lives under `app/` subdir)
|
|
71
|
+
- `v2/core/<name>/` — core modules (code lives at the module root)
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
#### `module-structure`
|
|
76
|
+
|
|
77
|
+
Enforces the allowed file/folder layout inside v2 modules.
|
|
78
|
+
|
|
79
|
+
**Allowed paths (business modules):**
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
app/contexts/*.(ts|tsx)
|
|
83
|
+
app/constants/*.ts
|
|
84
|
+
app/hooks/*.ts
|
|
85
|
+
app/stores/*.ts
|
|
86
|
+
app/types/*.ts
|
|
87
|
+
app/utils/*.ts
|
|
88
|
+
app/graphql/{fragments,queries,mutations}/*.(gql|ts)
|
|
89
|
+
app/ui/{atoms,molecules,organisms,templates}/*.(vue|tsx)
|
|
90
|
+
app/ui/assets/fonts/*.(woff|woff2|ttf|eot)
|
|
91
|
+
app/ui/assets/icons/*.(vue|tsx)
|
|
92
|
+
app/ui/assets/images/*.(png|jpg|jpeg|gif|svg|webp|ico|bmp|tiff)
|
|
93
|
+
app/ui/assets/styles/*.(css|scss)
|
|
94
|
+
app/ui/assets/videos/*.(mp4|mp3)
|
|
95
|
+
*.test.[tj]sx? (anywhere in the module)
|
|
96
|
+
external.ts (module root only)
|
|
97
|
+
index.ts (module root only)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Core modules use the same layout without the `app/` prefix.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
#### `no-module-barrel`
|
|
105
|
+
|
|
106
|
+
Bans `index.ts` barrel files inside module subdirectories. Only the module-root `index.ts` is allowed.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// ❌ Error — app/hooks/index.ts
|
|
110
|
+
export { useFoo } from './useFoo';
|
|
111
|
+
|
|
112
|
+
// ✅ OK — module-root index.ts
|
|
113
|
+
export { useFoo } from './app/hooks/useFoo';
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
#### `module-import-boundary`
|
|
119
|
+
|
|
120
|
+
Prevents files inside a module from importing across the module boundary. Cross-module dependencies must go through `external.ts`.
|
|
121
|
+
|
|
122
|
+
Banned imports:
|
|
123
|
+
- Alias imports (`~/...`, `@/...`) — always escape the module
|
|
124
|
+
- Relative imports (`../`) resolving outside the module root
|
|
125
|
+
|
|
126
|
+
Scoped npm packages (e.g. `@shopify/react-form`) are unaffected — the alias check only matches the literal `@/` prefix.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// ❌ Error
|
|
130
|
+
import { useShop } from '~/modules/shop-info/app/hooks/useShop';
|
|
131
|
+
|
|
132
|
+
// ✅ OK — through the public API
|
|
133
|
+
import { useShop } from '~/modules/shop-info';
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
#### `pure-ui-layer`
|
|
139
|
+
|
|
140
|
+
Bans hook imports and `use*` calls inside `ui/atoms/` and `ui/molecules/`. These layers must be stateless.
|
|
141
|
+
|
|
142
|
+
Exempt hooks: anything matching `use.*i18n`, `use.*translat`, or `use.*const` (case-insensitive) — e.g. `useI18n`, `useTranslation`, `useConstants`.
|
|
143
|
+
|
|
144
|
+
Files under `v2/modules/<name>/core/` are fully exempt from this rule.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
// ❌ Error — atoms/ProductCard.tsx
|
|
148
|
+
import { useProduct } from '../../hooks/useProduct';
|
|
149
|
+
|
|
150
|
+
// ✅ OK
|
|
151
|
+
import { useTranslation } from '../../hooks/useTranslation';
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
#### `module-index-required`
|
|
157
|
+
|
|
158
|
+
Verifies that every v2 module has an `index.ts` at its root. Fires on every file linted inside the module.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### i18n (React + Vue)
|
|
163
|
+
|
|
164
|
+
#### `no-t-for-variable`
|
|
165
|
+
|
|
166
|
+
Only plain string literals are allowed as the first argument to `t()`. Template literals with interpolations are banned.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// ✅ OK
|
|
170
|
+
t("Save")
|
|
171
|
+
t(`Hello`)
|
|
172
|
+
|
|
173
|
+
// ❌ Error
|
|
174
|
+
t(key)
|
|
175
|
+
t(`Hello ${name}`)
|
|
176
|
+
t(props.label)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### Vue-specific
|
|
182
|
+
|
|
183
|
+
#### `not-use-store-to-refs`
|
|
184
|
+
|
|
185
|
+
Bans Pinia's `storeToRefs()`. Use `computed()` instead for explicit reactivity.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// ❌ Error
|
|
189
|
+
const { count } = storeToRefs(useCounterStore())
|
|
190
|
+
|
|
191
|
+
// ✅ OK
|
|
192
|
+
const count = computed(() => useCounterStore().count)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### `not-use-custom-color-class`
|
|
196
|
+
|
|
197
|
+
Warns when raw color values (hex, `rgb()`, `hsl()`) are used in Vue template class bindings. Use design-system tokens instead.
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
<!-- ❌ Warning -->
|
|
201
|
+
<div :class="['[#ff0000]']" />
|
|
202
|
+
|
|
203
|
+
<!-- ✅ OK -->
|
|
204
|
+
<div class="text-primary" />
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Requires `vue-eslint-parser` as the parser.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### React / TypeScript-specific
|
|
212
|
+
|
|
213
|
+
#### `no-export-type-from-tsx`
|
|
214
|
+
|
|
215
|
+
Disallows type/interface exports from `.tsx` files. Move types to a dedicated `.ts` file.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// ❌ Error — MyComponent.tsx
|
|
219
|
+
export type MyProps = { name: string };
|
|
220
|
+
|
|
221
|
+
// ✅ OK — MyComponent.ts
|
|
222
|
+
export type MyProps = { name: string };
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### `no-import-from-package-path`
|
|
226
|
+
|
|
227
|
+
Prevents value imports from `@gem-sdk` packages using deep path syntax. Type-only imports are allowed.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// ❌ Error
|
|
231
|
+
import { fn } from '@gem-sdk/core/internal/utils';
|
|
232
|
+
|
|
233
|
+
// ✅ OK
|
|
234
|
+
import { fn } from '@gem-sdk/core';
|
|
235
|
+
import type { T } from '@gem-sdk/core/internal/utils';
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Development
|
|
241
|
+
|
|
242
|
+
### Setup
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
yarn install
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Tests
|
|
249
|
+
|
|
250
|
+
Each rule has a co-located `.test.ts` file. Run all tests with:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
yarn test
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Build
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
yarn build # outputs to dist/
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Adding a new rule
|
|
263
|
+
|
|
264
|
+
1. Create `src/rules/myNewRule.ts` (camelCase filename)
|
|
265
|
+
2. Export the rule as `default`
|
|
266
|
+
3. Add a co-located `src/rules/myNewRule.test.ts`
|
|
267
|
+
4. Register in `src/index.ts`:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import myNewRule from './rules/myNewRule';
|
|
271
|
+
|
|
272
|
+
export const rules = {
|
|
273
|
+
// ...existing
|
|
274
|
+
'my-new-rule': myNewRule,
|
|
275
|
+
};
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Release
|
|
281
|
+
|
|
282
|
+
This package uses [Changesets](https://github.com/changesets/changesets) with the same branching strategy as `web-builder-elements`:
|
|
283
|
+
|
|
284
|
+
| Branch | NPM tag |
|
|
285
|
+
|--------|---------|
|
|
286
|
+
| `production` | `latest` |
|
|
287
|
+
| `staging` | `staging` |
|
|
288
|
+
| `dev` | `dev` |
|
|
289
|
+
| `dev-sun` / `dev-moon` / `dev-earth` / `dev-new-feature` | `sun` / `moon` / `earth` / `new-feature` |
|
|
290
|
+
|
|
291
|
+
To release a new version:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
# 1. Create a changeset describing your changes
|
|
295
|
+
yarn changeset
|
|
296
|
+
|
|
297
|
+
# 2. The CI release workflow handles publishing automatically on merge
|
|
298
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export declare const rules: {
|
|
2
|
+
'module-structure': import("eslint").Rule.RuleModule;
|
|
3
|
+
'no-module-barrel': import("eslint").Rule.RuleModule;
|
|
4
|
+
'module-import-boundary': import("eslint").Rule.RuleModule;
|
|
5
|
+
'pure-ui-layer': import("eslint").Rule.RuleModule;
|
|
6
|
+
'module-index-required': import("eslint").Rule.RuleModule;
|
|
7
|
+
'no-t-for-variable': import("eslint").Rule.RuleModule;
|
|
8
|
+
'not-use-store-to-refs': import("eslint").Rule.RuleModule;
|
|
9
|
+
'not-use-custom-color-class': import("eslint").Rule.RuleModule;
|
|
10
|
+
'no-export-type-from-tsx': import("eslint").Rule.RuleModule;
|
|
11
|
+
'no-import-from-package-path': import("eslint").Rule.RuleModule;
|
|
12
|
+
};
|
|
13
|
+
export declare const configs: {
|
|
14
|
+
recommended: {
|
|
15
|
+
readonly plugins: readonly ['gem-fe'];
|
|
16
|
+
readonly rules: {
|
|
17
|
+
readonly 'gem-fe/module-structure': 'error';
|
|
18
|
+
readonly 'gem-fe/no-module-barrel': 'error';
|
|
19
|
+
readonly 'gem-fe/module-import-boundary': 'error';
|
|
20
|
+
readonly 'gem-fe/pure-ui-layer': 'error';
|
|
21
|
+
readonly 'gem-fe/module-index-required': 'error';
|
|
22
|
+
readonly 'gem-fe/no-t-for-variable': 'error';
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
vue: {
|
|
26
|
+
readonly plugins: readonly ['gem-fe'];
|
|
27
|
+
readonly rules: {
|
|
28
|
+
readonly 'gem-fe/module-structure': 'error';
|
|
29
|
+
readonly 'gem-fe/no-module-barrel': 'error';
|
|
30
|
+
readonly 'gem-fe/module-import-boundary': 'error';
|
|
31
|
+
readonly 'gem-fe/pure-ui-layer': 'error';
|
|
32
|
+
readonly 'gem-fe/module-index-required': 'error';
|
|
33
|
+
readonly 'gem-fe/no-t-for-variable': 'error';
|
|
34
|
+
readonly 'gem-fe/not-use-store-to-refs': 'error';
|
|
35
|
+
readonly 'gem-fe/not-use-custom-color-class': 'warn';
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
react: {
|
|
39
|
+
readonly plugins: readonly ['gem-fe'];
|
|
40
|
+
readonly rules: {
|
|
41
|
+
readonly 'gem-fe/module-structure': 'error';
|
|
42
|
+
readonly 'gem-fe/no-module-barrel': 'error';
|
|
43
|
+
readonly 'gem-fe/module-import-boundary': 'error';
|
|
44
|
+
readonly 'gem-fe/pure-ui-layer': 'error';
|
|
45
|
+
readonly 'gem-fe/module-index-required': 'error';
|
|
46
|
+
readonly 'gem-fe/no-t-for-variable': 'error';
|
|
47
|
+
readonly 'gem-fe/no-export-type-from-tsx': 'error';
|
|
48
|
+
readonly 'gem-fe/no-import-from-package-path': 'error';
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,KAAK;;;;;;;;;;;CAegB,CAAC;AAyCnC,eAAO,MAAM,OAAO;;oCAnCR,QAAQ;;qBAEhB,yBAAyB,EAAE,OAAO;qBAClC,yBAAyB,EAAE,OAAO;qBAClC,+BAA+B,EAAE,OAAO;qBACxC,sBAAsB,EAAE,OAAO;qBAC/B,8BAA8B,EAAE,OAAO;qBACvC,0BAA0B,EAAE,OAAO;;;;oCAQ3B,QAAQ;;gDAbW,OAAO;gDACP,OAAO;sDACD,OAAO;6CAChB,OAAO;qDACC,OAAO;iDACX,OAAO;qDAWH,OAAO;0DACF,MAAM;;;;oCAQnC,QAAQ;;gDAzBW,OAAO;gDACP,OAAO;sDACD,OAAO;6CAChB,OAAO;qDACC,OAAO;iDACX,OAAO;uDAuBD,OAAO;2DACH,OAAO;;;CAIC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.configs = exports.rules = void 0;
|
|
7
|
+
const moduleImportBoundary_1 = __importDefault(require("./rules/moduleImportBoundary"));
|
|
8
|
+
const moduleIndexRequired_1 = __importDefault(require("./rules/moduleIndexRequired"));
|
|
9
|
+
const moduleStructure_1 = __importDefault(require("./rules/moduleStructure"));
|
|
10
|
+
const noExportTypeFromTsx_1 = __importDefault(require("./rules/noExportTypeFromTsx"));
|
|
11
|
+
const noImportFromPackagePath_1 = __importDefault(require("./rules/noImportFromPackagePath"));
|
|
12
|
+
const noModuleBarrel_1 = __importDefault(require("./rules/noModuleBarrel"));
|
|
13
|
+
const noTForVariable_1 = __importDefault(require("./rules/noTForVariable"));
|
|
14
|
+
const notUseCustomColorClass_1 = __importDefault(require("./rules/notUseCustomColorClass"));
|
|
15
|
+
const notUseStoreToRefs_1 = __importDefault(require("./rules/notUseStoreToRefs"));
|
|
16
|
+
const pureUiLayer_1 = __importDefault(require("./rules/pureUiLayer"));
|
|
17
|
+
exports.rules = {
|
|
18
|
+
// Architecture — shared between React and Vue (file extensions target both)
|
|
19
|
+
'module-structure': moduleStructure_1.default,
|
|
20
|
+
'no-module-barrel': noModuleBarrel_1.default,
|
|
21
|
+
'module-import-boundary': moduleImportBoundary_1.default,
|
|
22
|
+
'pure-ui-layer': pureUiLayer_1.default,
|
|
23
|
+
'module-index-required': moduleIndexRequired_1.default,
|
|
24
|
+
// i18n
|
|
25
|
+
'no-t-for-variable': noTForVariable_1.default,
|
|
26
|
+
// Vue-specific
|
|
27
|
+
'not-use-store-to-refs': notUseStoreToRefs_1.default,
|
|
28
|
+
'not-use-custom-color-class': notUseCustomColorClass_1.default,
|
|
29
|
+
// React/TypeScript-specific
|
|
30
|
+
'no-export-type-from-tsx': noExportTypeFromTsx_1.default,
|
|
31
|
+
'no-import-from-package-path': noImportFromPackagePath_1.default,
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Shared architecture + i18n rules for both React and Vue repos.
|
|
35
|
+
*/
|
|
36
|
+
const recommended = {
|
|
37
|
+
plugins: ['gem-fe'],
|
|
38
|
+
rules: {
|
|
39
|
+
'gem-fe/module-structure': 'error',
|
|
40
|
+
'gem-fe/no-module-barrel': 'error',
|
|
41
|
+
'gem-fe/module-import-boundary': 'error',
|
|
42
|
+
'gem-fe/pure-ui-layer': 'error',
|
|
43
|
+
'gem-fe/module-index-required': 'error',
|
|
44
|
+
'gem-fe/no-t-for-variable': 'error',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* `recommended` + Vue-specific rules. Use with vue-eslint-parser.
|
|
49
|
+
*/
|
|
50
|
+
const vue = {
|
|
51
|
+
plugins: ['gem-fe'],
|
|
52
|
+
rules: {
|
|
53
|
+
...recommended.rules,
|
|
54
|
+
'gem-fe/not-use-store-to-refs': 'error',
|
|
55
|
+
'gem-fe/not-use-custom-color-class': 'warn',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* `recommended` + React/TypeScript-specific rules.
|
|
60
|
+
*/
|
|
61
|
+
const react = {
|
|
62
|
+
plugins: ['gem-fe'],
|
|
63
|
+
rules: {
|
|
64
|
+
...recommended.rules,
|
|
65
|
+
'gem-fe/no-export-type-from-tsx': 'error',
|
|
66
|
+
'gem-fe/no-import-from-package-path': 'error',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
exports.configs = { recommended, vue, react };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects whether a file lives inside a v2 module.
|
|
3
|
+
*
|
|
4
|
+
* Handles both directory shapes used across GemPages repos:
|
|
5
|
+
* v2/modules/<name>/ — business modules, internal code under app/ subdir
|
|
6
|
+
* v2/core/<name>/ — core modules, internal code at module root
|
|
7
|
+
*
|
|
8
|
+
* The regex matches both:
|
|
9
|
+
* app/v2/modules/<name>/ (web-builder-shopify-app)
|
|
10
|
+
* apps/gemx/src/v2/modules/<name>/ (web-builder)
|
|
11
|
+
*
|
|
12
|
+
* Returns null when the file is not inside a recognised module, including when
|
|
13
|
+
* the file lives under a core/ subfolder of a business module (v2/modules/<name>/core/).
|
|
14
|
+
* Such files are exempt from all module lint rules.
|
|
15
|
+
*/
|
|
16
|
+
export type ModuleInfo = {
|
|
17
|
+
moduleRoot: string;
|
|
18
|
+
type: 'app' | 'core';
|
|
19
|
+
moduleName: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Returns true for files under a core/ subdirectory of a business module,
|
|
23
|
+
* e.g. v2/modules/<name>/core/**. These files are exempt from module lint rules.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isModuleCoreSubfolder(filePath: string): boolean;
|
|
26
|
+
export declare function getModuleInfo(filePath: string): ModuleInfo | null;
|
|
27
|
+
//# sourceMappingURL=moduleInfo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moduleInfo.d.ts","sourceRoot":"","sources":["../../src/lib/moduleInfo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAUjE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isModuleCoreSubfolder = isModuleCoreSubfolder;
|
|
4
|
+
exports.getModuleInfo = getModuleInfo;
|
|
5
|
+
/**
|
|
6
|
+
* Returns true for files under a core/ subdirectory of a business module,
|
|
7
|
+
* e.g. v2/modules/<name>/core/**. These files are exempt from module lint rules.
|
|
8
|
+
*/
|
|
9
|
+
function isModuleCoreSubfolder(filePath) {
|
|
10
|
+
return /\/v2\/modules\/[^/]+\/core(?:\/|$)/.test(filePath.replace(/\\/g, '/'));
|
|
11
|
+
}
|
|
12
|
+
function getModuleInfo(filePath) {
|
|
13
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
14
|
+
if (isModuleCoreSubfolder(normalized))
|
|
15
|
+
return null;
|
|
16
|
+
const match = normalized.match(/^(.*\/v2\/(modules|core)\/([^/]+))(\/.*)?$/);
|
|
17
|
+
if (!match)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
moduleRoot: match[1],
|
|
21
|
+
type: match[2] === 'modules' ? 'app' : 'core',
|
|
22
|
+
moduleName: match[3],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moduleImportBoundary.d.ts","sourceRoot":"","sources":["../../src/rules/moduleImportBoundary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAoBnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAiEhB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
7
|
+
const moduleInfo_1 = require("../lib/moduleInfo");
|
|
8
|
+
/**
|
|
9
|
+
* Only external.ts is allowed to reach outside the module boundary.
|
|
10
|
+
*
|
|
11
|
+
* Two kinds of cross-module specifiers are banned for all other files:
|
|
12
|
+
* - alias imports (~/ and @/) — always point outside the module.
|
|
13
|
+
* Note: @scope/pkg is unaffected because `@/` requires a literal slash
|
|
14
|
+
* immediately after `@`, so `@shopify/form` does NOT match.
|
|
15
|
+
* - relative imports (../) that resolve outside the module root.
|
|
16
|
+
*
|
|
17
|
+
* The check applies to every construct that can pull in a module specifier:
|
|
18
|
+
* - import declarations: import x from '~/other'
|
|
19
|
+
* - re-exports: export { x } from '~/other' / export * from '@/o'
|
|
20
|
+
* - dynamic imports: import('~/other')
|
|
21
|
+
*/
|
|
22
|
+
const ALIAS_RE = /^(?:~|@)\//;
|
|
23
|
+
const rule = {
|
|
24
|
+
meta: {
|
|
25
|
+
type: 'problem',
|
|
26
|
+
docs: {
|
|
27
|
+
description: 'Prevent cross-module imports (alias ~//@/ or relative escapes) — only external.ts may import from other modules',
|
|
28
|
+
},
|
|
29
|
+
messages: {
|
|
30
|
+
noOutsideImport: 'Import "{{source}}" resolves outside this module\'s boundary. ' +
|
|
31
|
+
'Route cross-module dependencies through external.ts instead.',
|
|
32
|
+
noAliasImport: 'Alias import "{{source}}" points outside this module. ' +
|
|
33
|
+
'Route cross-module dependencies through external.ts instead.',
|
|
34
|
+
},
|
|
35
|
+
schema: [],
|
|
36
|
+
},
|
|
37
|
+
create(context) {
|
|
38
|
+
const filePath = context.getFilename().replace(/\\/g, '/');
|
|
39
|
+
if (filePath.endsWith('/external.ts'))
|
|
40
|
+
return {};
|
|
41
|
+
const info = (0, moduleInfo_1.getModuleInfo)(filePath);
|
|
42
|
+
if (!info)
|
|
43
|
+
return {};
|
|
44
|
+
const moduleRoot = info.moduleRoot;
|
|
45
|
+
const fileDir = node_path_1.default.dirname(filePath);
|
|
46
|
+
function checkSource(node, source) {
|
|
47
|
+
if (typeof source !== 'string')
|
|
48
|
+
return;
|
|
49
|
+
// Alias imports (~/, @/) always escape the module — ban them.
|
|
50
|
+
if (ALIAS_RE.test(source)) {
|
|
51
|
+
context.report({ node, messageId: 'noAliasImport', data: { source } });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Only relative specifiers remain to be checked — npm packages are fine.
|
|
55
|
+
if (!source.startsWith('.'))
|
|
56
|
+
return;
|
|
57
|
+
const resolved = node_path_1.default.resolve(fileDir, source).replace(/\\/g, '/');
|
|
58
|
+
if (!resolved.startsWith(`${moduleRoot}/`) && resolved !== moduleRoot) {
|
|
59
|
+
context.report({ node, messageId: 'noOutsideImport', data: { source } });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
// import x from '...'
|
|
64
|
+
ImportDeclaration(node) {
|
|
65
|
+
checkSource(node, node.source.value);
|
|
66
|
+
},
|
|
67
|
+
// export { x } from '...' (source is null for local `export { x }`)
|
|
68
|
+
ExportNamedDeclaration(node) {
|
|
69
|
+
if (node.source)
|
|
70
|
+
checkSource(node, node.source.value);
|
|
71
|
+
},
|
|
72
|
+
// export * from '...'
|
|
73
|
+
ExportAllDeclaration(node) {
|
|
74
|
+
checkSource(node, node.source.value);
|
|
75
|
+
},
|
|
76
|
+
// import('...') — only when the specifier is a string literal
|
|
77
|
+
ImportExpression(node) {
|
|
78
|
+
if (node.source.type === 'Literal')
|
|
79
|
+
checkSource(node, node.source.value);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
exports.default = rule;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
/**
|
|
3
|
+
* When linting any file inside a module, verify that the module root contains
|
|
4
|
+
* an index.ts. ESLint lints files rather than directories, so this rule fires
|
|
5
|
+
* per-file — if no file inside a missing-index module is ever linted the gap
|
|
6
|
+
* won't be caught, but in practice CI lints all files.
|
|
7
|
+
*/
|
|
8
|
+
declare const rule: Rule.RuleModule;
|
|
9
|
+
export default rule;
|
|
10
|
+
//# sourceMappingURL=moduleIndexRequired.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moduleIndexRequired.d.ts","sourceRoot":"","sources":["../../src/rules/moduleIndexRequired.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAKnC;;;;;GAKG;AACH,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAkChB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const moduleInfo_1 = require("../lib/moduleInfo");
|
|
9
|
+
/**
|
|
10
|
+
* When linting any file inside a module, verify that the module root contains
|
|
11
|
+
* an index.ts. ESLint lints files rather than directories, so this rule fires
|
|
12
|
+
* per-file — if no file inside a missing-index module is ever linted the gap
|
|
13
|
+
* won't be caught, but in practice CI lints all files.
|
|
14
|
+
*/
|
|
15
|
+
const rule = {
|
|
16
|
+
meta: {
|
|
17
|
+
type: 'problem',
|
|
18
|
+
docs: {
|
|
19
|
+
description: 'Every module must expose a public API via index.ts at its root',
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
missingIndex: 'Module "{{moduleName}}" has no index.ts at its root ({{moduleRoot}}/). ' +
|
|
23
|
+
'Every module must expose a public API through index.ts.',
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
Program(node) {
|
|
30
|
+
const filePath = context.getFilename().replace(/\\/g, '/');
|
|
31
|
+
const info = (0, moduleInfo_1.getModuleInfo)(filePath);
|
|
32
|
+
if (!info)
|
|
33
|
+
return;
|
|
34
|
+
// Don't double-report when linting index.ts itself
|
|
35
|
+
if (filePath === `${info.moduleRoot}/index.ts`)
|
|
36
|
+
return;
|
|
37
|
+
const indexPath = node_path_1.default.join(info.moduleRoot, 'index.ts');
|
|
38
|
+
if (!node_fs_1.default.existsSync(indexPath)) {
|
|
39
|
+
context.report({
|
|
40
|
+
node,
|
|
41
|
+
messageId: 'missingIndex',
|
|
42
|
+
data: { moduleName: info.moduleName, moduleRoot: info.moduleRoot },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
exports.default = rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moduleStructure.d.ts","sourceRoot":"","sources":["../../src/rules/moduleStructure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAkDnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA4BhB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const moduleInfo_1 = require("../lib/moduleInfo");
|
|
4
|
+
/**
|
|
5
|
+
* Builds the list of allowed path patterns relative to the module root.
|
|
6
|
+
* Assets are organised into typed subdirectories.
|
|
7
|
+
* Both .tsx (React) and .vue (Vue) are accepted for UI components and icons.
|
|
8
|
+
*
|
|
9
|
+
* @param prefix 'app' for business modules, '' for core modules
|
|
10
|
+
*/
|
|
11
|
+
function buildPatterns(prefix) {
|
|
12
|
+
const p = prefix ? `${prefix}/` : '';
|
|
13
|
+
return [
|
|
14
|
+
/\.test\.[tj]sx?$/,
|
|
15
|
+
new RegExp(`^${p}contexts/[^/]+\\.(ts|tsx)$`),
|
|
16
|
+
new RegExp(`^${p}constants/[^/]+\\.ts$`),
|
|
17
|
+
new RegExp(`^${p}hooks/[^/]+\\.ts$`),
|
|
18
|
+
new RegExp(`^${p}stores/[^/]+\\.ts$`),
|
|
19
|
+
new RegExp(`^${p}types/[^/]+\\.ts$`),
|
|
20
|
+
new RegExp(`^${p}utils/[^/]+\\.ts$`),
|
|
21
|
+
new RegExp(`^${p}graphql/(fragments|queries|mutations)/[^/]+\\.(gql|ts)$`),
|
|
22
|
+
// Assets — categorised subdirectories
|
|
23
|
+
new RegExp(`^${p}ui/assets/fonts/[^/]+\\.(woff|woff2|ttf|eot)$`),
|
|
24
|
+
new RegExp(`^${p}ui/assets/icons/[^/]+\\.(vue|tsx)$`),
|
|
25
|
+
new RegExp(`^${p}ui/assets/images/[^/]+\\.(png|jpg|jpeg|gif|svg|webp|ico|bmp|tiff)$`),
|
|
26
|
+
new RegExp(`^${p}ui/assets/styles/[^/]+\\.(css|scss)$`),
|
|
27
|
+
new RegExp(`^${p}ui/assets/videos/[^/]+\\.(mp4|mp3)$`),
|
|
28
|
+
// UI layer components
|
|
29
|
+
new RegExp(`^${p}ui/atoms/[^/]+\\.(vue|tsx)$`),
|
|
30
|
+
new RegExp(`^${p}ui/molecules/[^/]+\\.(vue|tsx)$`),
|
|
31
|
+
new RegExp(`^${p}ui/organisms/[^/]+\\.(vue|tsx)$`),
|
|
32
|
+
new RegExp(`^${p}ui/templates/[^/]+\\.(vue|tsx)$`),
|
|
33
|
+
// Module boundary files (no prefix — always at module root)
|
|
34
|
+
/^external\.ts$/,
|
|
35
|
+
/^index\.ts$/,
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
const PATTERNS = {
|
|
39
|
+
app: buildPatterns('app'),
|
|
40
|
+
core: buildPatterns(''),
|
|
41
|
+
};
|
|
42
|
+
const ALLOWED_PATHS_HINT = 'contexts/*.(ts|tsx), constants/*.ts, graphql/{fragments,queries,mutations}/*.(gql|ts), ' +
|
|
43
|
+
'hooks/*.ts, stores/*.ts, types/*.ts, utils/*.ts, ' +
|
|
44
|
+
'ui/{atoms,molecules,organisms,templates}/*.(vue|tsx), ' +
|
|
45
|
+
'ui/assets/{fonts,icons,images,styles,videos}/*, ' +
|
|
46
|
+
'external.ts, index.ts';
|
|
47
|
+
const rule = {
|
|
48
|
+
meta: {
|
|
49
|
+
type: 'problem',
|
|
50
|
+
docs: {
|
|
51
|
+
description: 'Enforce allowed file/folder structure within modules — no arbitrary nesting',
|
|
52
|
+
},
|
|
53
|
+
messages: {
|
|
54
|
+
invalidPath: '"{{relPath}}" is not an allowed location inside this module.\n' + `Allowed paths: ${ALLOWED_PATHS_HINT}`,
|
|
55
|
+
},
|
|
56
|
+
schema: [],
|
|
57
|
+
},
|
|
58
|
+
create(context) {
|
|
59
|
+
return {
|
|
60
|
+
Program(node) {
|
|
61
|
+
const filePath = context.getFilename().replace(/\\/g, '/');
|
|
62
|
+
const info = (0, moduleInfo_1.getModuleInfo)(filePath);
|
|
63
|
+
if (!info)
|
|
64
|
+
return;
|
|
65
|
+
const relPath = filePath.slice(info.moduleRoot.length + 1);
|
|
66
|
+
const patterns = PATTERNS[info.type] ?? PATTERNS.core;
|
|
67
|
+
if (!patterns.some((p) => p.test(relPath))) {
|
|
68
|
+
context.report({ node, messageId: 'invalidPath', data: { relPath } });
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
exports.default = rule;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
/**
|
|
3
|
+
* Disallows exporting types and interfaces from .tsx files.
|
|
4
|
+
* Types should live in a dedicated .ts file.
|
|
5
|
+
*/
|
|
6
|
+
declare const rule: Rule.RuleModule;
|
|
7
|
+
export default rule;
|
|
8
|
+
//# sourceMappingURL=noExportTypeFromTsx.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noExportTypeFromTsx.d.ts","sourceRoot":"","sources":["../../src/rules/noExportTypeFromTsx.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEnC;;;GAGG;AACH,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAqBhB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Disallows exporting types and interfaces from .tsx files.
|
|
5
|
+
* Types should live in a dedicated .ts file.
|
|
6
|
+
*/
|
|
7
|
+
const rule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Disallow export type and interface declarations from .tsx files',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
noTypeExport: 'Exporting types and interfaces from .tsx files is not allowed. Move them to a .ts file.',
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
ExportNamedDeclaration(node) {
|
|
21
|
+
const fileName = context.getFilename();
|
|
22
|
+
if (fileName.endsWith('.tsx') && node.exportKind === 'type') {
|
|
23
|
+
context.report({ node, messageId: 'noTypeExport' });
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
exports.default = rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noImportFromPackagePath.d.ts","sourceRoot":"","sources":["../../src/rules/noImportFromPackagePath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAYnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA2BhB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Disallows value imports from @gem-sdk packages using deep path syntax.
|
|
5
|
+
* Type-only imports are allowed since they are erased at runtime.
|
|
6
|
+
*
|
|
7
|
+
* Bad: import { fn } from '@gem-sdk/core/some/path'
|
|
8
|
+
* OK: import { fn } from '@gem-sdk/core'
|
|
9
|
+
* OK: import type { T } from '@gem-sdk/core/some/path'
|
|
10
|
+
*/
|
|
11
|
+
const GEM_SDK_PATH_RE = /@gem-sdk\/[a-z-]+\/.+/;
|
|
12
|
+
const rule = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Disallow value imports from @gem-sdk packages using deep path syntax',
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
noPathImport: 'Import from packages with a deep path is not allowed. ' +
|
|
20
|
+
'Import from the package root instead: "{{root}}".',
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
ImportDeclaration(node) {
|
|
27
|
+
const { importKind } = node;
|
|
28
|
+
const source = node.source.value;
|
|
29
|
+
if (typeof source !== 'string')
|
|
30
|
+
return;
|
|
31
|
+
if (importKind === 'type')
|
|
32
|
+
return;
|
|
33
|
+
if (!GEM_SDK_PATH_RE.test(source))
|
|
34
|
+
return;
|
|
35
|
+
const root = source.split('/').slice(0, 2).join('/');
|
|
36
|
+
context.report({ node, messageId: 'noPathImport', data: { root } });
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
exports.default = rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noModuleBarrel.d.ts","sourceRoot":"","sources":["../../src/rules/noModuleBarrel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA6BhB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const moduleInfo_1 = require("../lib/moduleInfo");
|
|
4
|
+
const rule = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Prevent barrel index.ts files inside module subdirectories — only the module root index.ts is allowed',
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
noBarrel: 'Barrel index.ts is not allowed inside module subdirectories. ' +
|
|
12
|
+
'Only the module root index.ts is permitted. Export directly from the source file.',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
Program(node) {
|
|
19
|
+
const filePath = context.getFilename().replace(/\\/g, '/');
|
|
20
|
+
if (!filePath.endsWith('/index.ts'))
|
|
21
|
+
return;
|
|
22
|
+
const info = (0, moduleInfo_1.getModuleInfo)(filePath);
|
|
23
|
+
if (!info)
|
|
24
|
+
return;
|
|
25
|
+
if (filePath !== `${info.moduleRoot}/index.ts`) {
|
|
26
|
+
context.report({ node, messageId: 'noBarrel' });
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
exports.default = rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noTForVariable.d.ts","sourceRoot":"","sources":["../../src/rules/noTForVariable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AA+BnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA+BhB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Only plain string literals are allowed as the first argument to t().
|
|
5
|
+
* TemplateLiterals with expressions (e.g. `Hello ${name}`) are NOT allowed.
|
|
6
|
+
*/
|
|
7
|
+
function isPlainStringLiteral(node) {
|
|
8
|
+
if (node.type === 'Literal')
|
|
9
|
+
return true;
|
|
10
|
+
if (node.type === 'TemplateLiteral') {
|
|
11
|
+
const expressions = Array.isArray(node.expressions) ? node.expressions : [];
|
|
12
|
+
return expressions.length === 0;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function createCallExpressionHandler(context) {
|
|
17
|
+
return function (node) {
|
|
18
|
+
const callNode = node;
|
|
19
|
+
if (callNode.callee?.type !== 'Identifier' || callNode.callee?.name !== 't')
|
|
20
|
+
return;
|
|
21
|
+
const firstArg = callNode.arguments?.[0];
|
|
22
|
+
if (!firstArg || isPlainStringLiteral(firstArg))
|
|
23
|
+
return;
|
|
24
|
+
context.report({
|
|
25
|
+
node: callNode.callee,
|
|
26
|
+
messageId: 'noVariable',
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const rule = {
|
|
31
|
+
meta: {
|
|
32
|
+
type: 'problem',
|
|
33
|
+
docs: {
|
|
34
|
+
description: 'Do not call t(variable) — only plain string literals are allowed as the first argument',
|
|
35
|
+
},
|
|
36
|
+
messages: {
|
|
37
|
+
noVariable: 'Do not use t() with a variable or interpolated template as first argument. ' +
|
|
38
|
+
'Use a plain string literal key, e.g. t("Text") or t("Text {{name}}", { name }).',
|
|
39
|
+
},
|
|
40
|
+
schema: [],
|
|
41
|
+
},
|
|
42
|
+
create(context) {
|
|
43
|
+
const scriptVisitor = { CallExpression: createCallExpressionHandler(context) };
|
|
44
|
+
const templateBodyVisitor = { CallExpression: createCallExpressionHandler(context) };
|
|
45
|
+
const sourceCode = context.sourceCode ??
|
|
46
|
+
context.getSourceCode?.();
|
|
47
|
+
const parserServices = sourceCode && typeof sourceCode === 'object' && 'parserServices' in sourceCode
|
|
48
|
+
? sourceCode
|
|
49
|
+
.parserServices
|
|
50
|
+
: undefined;
|
|
51
|
+
if (parserServices?.defineTemplateBodyVisitor) {
|
|
52
|
+
return parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor);
|
|
53
|
+
}
|
|
54
|
+
return scriptVisitor;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
exports.default = rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notUseCustomColorClass.d.ts","sourceRoot":"","sources":["../../src/rules/notUseCustomColorClass.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AA0EnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAmDhB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Warns when raw color values (hex, rgb, hsl) are used in Vue template class bindings.
|
|
5
|
+
* Design-system tokens should be used instead.
|
|
6
|
+
*/
|
|
7
|
+
// Matches Tailwind arbitrary-value syntax with raw colors: [#fff], [rgb(...)], [hsl(...)]
|
|
8
|
+
const COLOR_VALUE_RE = /\[(#[^[\]]*?|rgb[^[\]]*?|hsl[^[\]]*?)\]/g;
|
|
9
|
+
function checkClassValue(context, node, value) {
|
|
10
|
+
let match;
|
|
11
|
+
COLOR_VALUE_RE.lastIndex = 0;
|
|
12
|
+
while ((match = COLOR_VALUE_RE.exec(value)) !== null) {
|
|
13
|
+
context.report({
|
|
14
|
+
node,
|
|
15
|
+
messageId: "noCustomColor",
|
|
16
|
+
data: { value: match[1] },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function walkNode(context, node) {
|
|
21
|
+
const n = node;
|
|
22
|
+
if (n.type !== "VAttribute")
|
|
23
|
+
return;
|
|
24
|
+
const isClassBinding = n.name === "class" ||
|
|
25
|
+
n.name === "className" ||
|
|
26
|
+
(n.name === "bind" &&
|
|
27
|
+
n.argument?.name ===
|
|
28
|
+
"class");
|
|
29
|
+
if (!isClassBinding)
|
|
30
|
+
return;
|
|
31
|
+
if (typeof n.value === "string") {
|
|
32
|
+
checkClassValue(context, node, n.value);
|
|
33
|
+
}
|
|
34
|
+
else if (n.value && typeof n.value === "object") {
|
|
35
|
+
const val = n.value;
|
|
36
|
+
if (typeof val.value === "string") {
|
|
37
|
+
checkClassValue(context, node, val.value);
|
|
38
|
+
}
|
|
39
|
+
if (val.expression?.type === "ArrayExpression") {
|
|
40
|
+
for (const el of val.expression.elements ?? []) {
|
|
41
|
+
if (el &&
|
|
42
|
+
typeof el === "object" &&
|
|
43
|
+
typeof el.value === "string") {
|
|
44
|
+
checkClassValue(context, node, el.value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const rule = {
|
|
51
|
+
meta: {
|
|
52
|
+
type: "suggestion",
|
|
53
|
+
docs: {
|
|
54
|
+
description: "Disallow raw color values in Vue template class bindings — use design-system tokens instead",
|
|
55
|
+
},
|
|
56
|
+
messages: {
|
|
57
|
+
noCustomColor: 'Raw color "{{value}}" detected in class binding. Use a design-system token instead (e.g. text-primary).',
|
|
58
|
+
},
|
|
59
|
+
schema: [],
|
|
60
|
+
},
|
|
61
|
+
create(context) {
|
|
62
|
+
const sourceCode = context
|
|
63
|
+
.sourceCode ??
|
|
64
|
+
context.getSourceCode?.();
|
|
65
|
+
const parserServices = sourceCode &&
|
|
66
|
+
typeof sourceCode === "object" &&
|
|
67
|
+
"parserServices" in sourceCode
|
|
68
|
+
? sourceCode.parserServices
|
|
69
|
+
: undefined;
|
|
70
|
+
if (!parserServices?.defineTemplateBodyVisitor)
|
|
71
|
+
return {};
|
|
72
|
+
return parserServices.defineTemplateBodyVisitor({
|
|
73
|
+
VElement(node) {
|
|
74
|
+
const el = node;
|
|
75
|
+
for (const attr of el.attributes ?? []) {
|
|
76
|
+
walkNode(context, attr);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
exports.default = rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notUseStoreToRefs.d.ts","sourceRoot":"","sources":["../../src/rules/notUseStoreToRefs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEnC;;GAEG;AACH,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAuBhB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Bans Pinia's storeToRefs() — use computed() instead to keep reactivity explicit.
|
|
5
|
+
*/
|
|
6
|
+
const rule = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Disallow storeToRefs() from Pinia — use computed() for explicit reactivity',
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
noStoreToRefs: 'Do not use storeToRefs(). Use computed() instead: ' +
|
|
14
|
+
'const count = computed(() => useCounterStore().count)',
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
CallExpression(node) {
|
|
21
|
+
const callee = node.callee;
|
|
22
|
+
if (callee.type === 'Identifier' && callee.name === 'storeToRefs') {
|
|
23
|
+
context.report({ node, messageId: 'noStoreToRefs' });
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
exports.default = rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pureUiLayer.d.ts","sourceRoot":"","sources":["../../src/rules/pureUiLayer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAcnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAyDhB,CAAC;eAEa,IAAI"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const moduleInfo_1 = require("../lib/moduleInfo");
|
|
4
|
+
const PURE_LAYER_DIRS = ['/ui/atoms/', '/ui/molecules/'];
|
|
5
|
+
const HOOK_NAME_RE = /^use[A-Z]/;
|
|
6
|
+
// i18n, translation, and constant hooks are read-only utilities allowed in pure layers.
|
|
7
|
+
const ALLOWED_HOOK_RE = /^use.*(?:i18n|translat|const)/i;
|
|
8
|
+
function isAllowedHook(name) {
|
|
9
|
+
return ALLOWED_HOOK_RE.test(name);
|
|
10
|
+
}
|
|
11
|
+
const rule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Prevent hook imports and use* calls inside ui/atoms and ui/molecules — these layers must be pure',
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
noHookImport: 'Importing from a hooks directory ("{{source}}") is not allowed in atoms or molecules. ' +
|
|
19
|
+
'Move stateful logic to an organism.',
|
|
20
|
+
noHookCall: '"{{name}}" looks like a hook (use* prefix) and is not allowed in atoms or molecules. ' +
|
|
21
|
+
'Move hook usage to an organism (only i18n/translation/const/constant hooks are exempt).',
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
const filePath = context.getFilename().replace(/\\/g, '/');
|
|
27
|
+
if ((0, moduleInfo_1.isModuleCoreSubfolder)(filePath))
|
|
28
|
+
return {};
|
|
29
|
+
if (!PURE_LAYER_DIRS.some((dir) => filePath.includes(dir)))
|
|
30
|
+
return {};
|
|
31
|
+
return {
|
|
32
|
+
ImportDeclaration(node) {
|
|
33
|
+
const source = node.source.value;
|
|
34
|
+
if (typeof source !== 'string')
|
|
35
|
+
return;
|
|
36
|
+
if (!/\/hooks(?:\/|$)/.test(source))
|
|
37
|
+
return;
|
|
38
|
+
const specifiers = node.specifiers;
|
|
39
|
+
const allAllowedHooks = specifiers.length > 0 &&
|
|
40
|
+
specifiers.every((s) => {
|
|
41
|
+
const name = s.type === 'ImportSpecifier'
|
|
42
|
+
? (s.imported.type === 'Identifier' ? s.imported.name : String(s.imported.value))
|
|
43
|
+
: s.local.name;
|
|
44
|
+
return isAllowedHook(name);
|
|
45
|
+
});
|
|
46
|
+
if (allAllowedHooks)
|
|
47
|
+
return;
|
|
48
|
+
context.report({ node, messageId: 'noHookImport', data: { source } });
|
|
49
|
+
},
|
|
50
|
+
CallExpression(node) {
|
|
51
|
+
const callee = node.callee;
|
|
52
|
+
let hookName = null;
|
|
53
|
+
if (callee.type === 'Identifier') {
|
|
54
|
+
hookName = callee.name;
|
|
55
|
+
}
|
|
56
|
+
else if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
57
|
+
hookName = callee.property.name;
|
|
58
|
+
}
|
|
59
|
+
if (hookName && HOOK_NAME_RE.test(hookName) && !isAllowedHook(hookName)) {
|
|
60
|
+
context.report({ node: callee, messageId: 'noHookCall', data: { name: hookName } });
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
exports.default = rule;
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gem-sdk/eslint-plugin",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Shared ESLint rules for GemPages frontend repos (React + Vue)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "rm -rf dist && tsc",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"type-check": "tsc --noEmit",
|
|
15
|
+
"lint": "eslint src --ext .ts",
|
|
16
|
+
"release": "changeset publish",
|
|
17
|
+
"release-packages": "yarn build && changeset publish"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"eslint": ">=8.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
24
|
+
"@changesets/cli": "^2.31.0",
|
|
25
|
+
"@types/eslint": "^8.56.12",
|
|
26
|
+
"@types/node": "^26.0.0",
|
|
27
|
+
"@typescript-eslint/parser": "^8.62.0",
|
|
28
|
+
"eslint": "8.57.0",
|
|
29
|
+
"typescript": "7.0.1-rc",
|
|
30
|
+
"vitest": "^3.2.6",
|
|
31
|
+
"vue-eslint-parser": "^9.4.3"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|