@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.
Files changed (38) hide show
  1. package/README.md +298 -0
  2. package/dist/index.d.ts +52 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +69 -0
  5. package/dist/lib/moduleInfo.d.ts +27 -0
  6. package/dist/lib/moduleInfo.d.ts.map +1 -0
  7. package/dist/lib/moduleInfo.js +24 -0
  8. package/dist/rules/moduleImportBoundary.d.ts +4 -0
  9. package/dist/rules/moduleImportBoundary.d.ts.map +1 -0
  10. package/dist/rules/moduleImportBoundary.js +84 -0
  11. package/dist/rules/moduleIndexRequired.d.ts +10 -0
  12. package/dist/rules/moduleIndexRequired.d.ts.map +1 -0
  13. package/dist/rules/moduleIndexRequired.js +49 -0
  14. package/dist/rules/moduleStructure.d.ts +4 -0
  15. package/dist/rules/moduleStructure.d.ts.map +1 -0
  16. package/dist/rules/moduleStructure.js +74 -0
  17. package/dist/rules/noExportTypeFromTsx.d.ts +8 -0
  18. package/dist/rules/noExportTypeFromTsx.d.ts.map +1 -0
  19. package/dist/rules/noExportTypeFromTsx.js +29 -0
  20. package/dist/rules/noImportFromPackagePath.d.ts +4 -0
  21. package/dist/rules/noImportFromPackagePath.d.ts.map +1 -0
  22. package/dist/rules/noImportFromPackagePath.js +41 -0
  23. package/dist/rules/noModuleBarrel.d.ts +4 -0
  24. package/dist/rules/noModuleBarrel.d.ts.map +1 -0
  25. package/dist/rules/noModuleBarrel.js +32 -0
  26. package/dist/rules/noTForVariable.d.ts +4 -0
  27. package/dist/rules/noTForVariable.d.ts.map +1 -0
  28. package/dist/rules/noTForVariable.js +57 -0
  29. package/dist/rules/notUseCustomColorClass.d.ts +4 -0
  30. package/dist/rules/notUseCustomColorClass.d.ts.map +1 -0
  31. package/dist/rules/notUseCustomColorClass.js +82 -0
  32. package/dist/rules/notUseStoreToRefs.d.ts +7 -0
  33. package/dist/rules/notUseStoreToRefs.d.ts.map +1 -0
  34. package/dist/rules/notUseStoreToRefs.js +29 -0
  35. package/dist/rules/pureUiLayer.d.ts +4 -0
  36. package/dist/rules/pureUiLayer.d.ts.map +1 -0
  37. package/dist/rules/pureUiLayer.js +66 -0
  38. 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
+ ```
@@ -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,4 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=moduleImportBoundary.d.ts.map
@@ -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,4 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=moduleStructure.d.ts.map
@@ -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,4 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=noImportFromPackagePath.d.ts.map
@@ -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,4 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=noModuleBarrel.d.ts.map
@@ -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,4 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=noTForVariable.d.ts.map
@@ -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,4 @@
1
+ import type { Rule } from "eslint";
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=notUseCustomColorClass.d.ts.map
@@ -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,7 @@
1
+ import type { Rule } from 'eslint';
2
+ /**
3
+ * Bans Pinia's storeToRefs() — use computed() instead to keep reactivity explicit.
4
+ */
5
+ declare const rule: Rule.RuleModule;
6
+ export default rule;
7
+ //# sourceMappingURL=notUseStoreToRefs.d.ts.map
@@ -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,4 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=pureUiLayer.d.ts.map
@@ -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
+ }