@shopify/shop-minis-react 0.1.7 → 0.1.8
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/dist/_virtual/index4.js +2 -2
- package/dist/_virtual/index5.js +3 -2
- package/dist/_virtual/index5.js.map +1 -1
- package/dist/_virtual/index6.js +2 -2
- package/dist/_virtual/index7.js +2 -3
- package/dist/_virtual/index7.js.map +1 -1
- package/dist/hooks/navigation/useDeeplink.js +9 -9
- package/dist/hooks/navigation/useDeeplink.js.map +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
- package/eslint/README.md +201 -0
- package/eslint/config.cjs +32 -0
- package/eslint/index.cjs +17 -0
- package/eslint/rules/no-internal-imports.cjs +43 -0
- package/eslint/rules/prefer-sdk-components.cjs +153 -0
- package/eslint/rules/validate-manifest.cjs +607 -0
- package/generated-hook-maps/hook-actions-map.json +130 -0
- package/generated-hook-maps/hook-scopes-map.json +48 -0
- package/package.json +9 -3
- package/src/hooks/navigation/useDeeplink.test.ts +429 -0
- package/src/hooks/navigation/useDeeplink.ts +6 -3
package/dist/_virtual/index4.js
CHANGED
package/dist/_virtual/index5.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index5.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index5.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|
package/dist/_virtual/index6.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { __require as r } from "../shop-minis-react/node_modules/.pnpm
|
|
1
|
+
import { __require as r } from "../shop-minis-react/node_modules/.pnpm/use-sync-external-store@1.5.0_react@19.1.0/node_modules/use-sync-external-store/shim/index.js";
|
|
2
2
|
var i = r();
|
|
3
3
|
export {
|
|
4
|
-
i as
|
|
4
|
+
i as s
|
|
5
5
|
};
|
|
6
6
|
//# sourceMappingURL=index6.js.map
|
package/dist/_virtual/index7.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
var i = r();
|
|
1
|
+
var r = {};
|
|
3
2
|
export {
|
|
4
|
-
|
|
3
|
+
r as __exports
|
|
5
4
|
};
|
|
6
5
|
//# sourceMappingURL=index7.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index7.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index7.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { useMemo as
|
|
2
|
-
import { parseUrl as
|
|
3
|
-
const
|
|
4
|
-
const { initialUrl: r } = window.minisParams;
|
|
5
|
-
return
|
|
1
|
+
import { useMemo as i } from "react";
|
|
2
|
+
import { parseUrl as n } from "../../utils/parseUrl.js";
|
|
3
|
+
const m = () => {
|
|
4
|
+
const { initialUrl: r, handle: e } = window.minisParams;
|
|
5
|
+
return i(() => {
|
|
6
6
|
if (!r)
|
|
7
7
|
return {
|
|
8
8
|
path: void 0,
|
|
9
9
|
queryParams: void 0,
|
|
10
10
|
hash: void 0
|
|
11
11
|
};
|
|
12
|
-
const a =
|
|
12
|
+
const a = n(r), t = `/mini/${e}`;
|
|
13
13
|
return {
|
|
14
|
-
path: a.pathname,
|
|
14
|
+
path: a.pathname.startsWith(t) ? a.pathname.replace(t, "") : a.pathname,
|
|
15
15
|
queryParams: a.query,
|
|
16
16
|
hash: a.hash
|
|
17
17
|
};
|
|
18
|
-
}, [r]);
|
|
18
|
+
}, [e, r]);
|
|
19
19
|
};
|
|
20
20
|
export {
|
|
21
|
-
|
|
21
|
+
m as useDeeplink
|
|
22
22
|
};
|
|
23
23
|
//# sourceMappingURL=useDeeplink.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDeeplink.js","sources":["../../../src/hooks/navigation/useDeeplink.ts"],"sourcesContent":["import {useMemo} from 'react'\n\nimport {parseUrl} from '../../utils/parseUrl'\n\ninterface UseDeeplinkReturnType {\n /**\n * The path of the deeplink.\n */\n path?: string\n /**\n * The query parameters of the deeplink.\n */\n queryParams?: {[key: string]: string | undefined}\n /**\n * The hash of the deeplink.\n */\n hash?: string\n}\n\nexport const useDeeplink = (): UseDeeplinkReturnType => {\n const {initialUrl} = window.minisParams\n\n return useMemo(() => {\n if (!initialUrl) {\n return {\n path: undefined,\n queryParams: undefined,\n hash: undefined,\n }\n }\n\n const parsedUrl = parseUrl(initialUrl)\n\n return {\n path: parsedUrl.pathname,\n queryParams: parsedUrl.query,\n hash: parsedUrl.hash,\n }\n }, [initialUrl])\n}\n"],"names":["useDeeplink","initialUrl","useMemo","parsedUrl","parseUrl"],"mappings":";;AAmBO,MAAMA,IAAc,MAA6B;
|
|
1
|
+
{"version":3,"file":"useDeeplink.js","sources":["../../../src/hooks/navigation/useDeeplink.ts"],"sourcesContent":["import {useMemo} from 'react'\n\nimport {parseUrl} from '../../utils/parseUrl'\n\ninterface UseDeeplinkReturnType {\n /**\n * The path of the deeplink.\n */\n path?: string\n /**\n * The query parameters of the deeplink.\n */\n queryParams?: {[key: string]: string | undefined}\n /**\n * The hash of the deeplink.\n */\n hash?: string\n}\n\nexport const useDeeplink = (): UseDeeplinkReturnType => {\n const {initialUrl, handle} = window.minisParams\n\n return useMemo(() => {\n if (!initialUrl) {\n return {\n path: undefined,\n queryParams: undefined,\n hash: undefined,\n }\n }\n\n const parsedUrl = parseUrl(initialUrl)\n const deeplinkPathnamePrefix = `/mini/${handle}`\n\n return {\n path: parsedUrl.pathname.startsWith(deeplinkPathnamePrefix)\n ? parsedUrl.pathname.replace(deeplinkPathnamePrefix, '')\n : parsedUrl.pathname,\n queryParams: parsedUrl.query,\n hash: parsedUrl.hash,\n }\n }, [handle, initialUrl])\n}\n"],"names":["useDeeplink","initialUrl","handle","useMemo","parsedUrl","parseUrl","deeplinkPathnamePrefix"],"mappings":";;AAmBO,MAAMA,IAAc,MAA6B;AACtD,QAAM,EAAC,YAAAC,GAAY,QAAAC,EAAM,IAAI,OAAO;AAEpC,SAAOC,EAAQ,MAAM;AACnB,QAAI,CAACF;AACI,aAAA;AAAA,QACL,MAAM;AAAA,QACN,aAAa;AAAA,QACb,MAAM;AAAA,MACR;AAGI,UAAAG,IAAYC,EAASJ,CAAU,GAC/BK,IAAyB,SAASJ,CAAM;AAEvC,WAAA;AAAA,MACL,MAAME,EAAU,SAAS,WAAWE,CAAsB,IACtDF,EAAU,SAAS,QAAQE,GAAwB,EAAE,IACrDF,EAAU;AAAA,MACd,aAAaA,EAAU;AAAA,MACvB,MAAMA,EAAU;AAAA,IAClB;AAAA,EAAA,GACC,CAACF,GAAQD,CAAU,CAAC;AACzB;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __module as q } from "../../../../../../../../_virtual/
|
|
1
|
+
import { __module as q } from "../../../../../../../../_virtual/index4.js";
|
|
2
2
|
import { __require as F } from "../../../../../global@4.4.0/node_modules/global/window.js";
|
|
3
3
|
import { __require as N } from "../../../../../@babel_runtime@7.27.6/node_modules/@babel/runtime/helpers/extends.js";
|
|
4
4
|
import { __require as J } from "../../../../../is-function@1.0.2/node_modules/is-function/index.js";
|
|
@@ -2,7 +2,7 @@ import L from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-ut
|
|
|
2
2
|
import T from "../../../../../../../_virtual/window.js";
|
|
3
3
|
import { forEachMediaGroup as Z } from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-utils/es/media-groups.js";
|
|
4
4
|
import J from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-utils/es/decode-b64-to-uint8-array.js";
|
|
5
|
-
import { l as Q } from "../../../../../../../_virtual/
|
|
5
|
+
import { l as Q } from "../../../../../../../_virtual/index5.js";
|
|
6
6
|
/*! @name mpd-parser @version 1.3.1 @license Apache-2.0 */
|
|
7
7
|
const w = (e) => !!e && typeof e == "object", E = (...e) => e.reduce((n, t) => (typeof t != "object" || Object.keys(t).forEach((r) => {
|
|
8
8
|
Array.isArray(n[r]) && Array.isArray(t[r]) ? n[r] = n[r].concat(t[r]) : w(n[r]) && w(t[r]) ? n[r] = E(n[r], t[r]) : n[r] = t[r];
|
package/eslint/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Shop Minis ESLint Plugin
|
|
2
|
+
|
|
3
|
+
Custom ESLint rules for Shop Minis apps. ESLint is included with the SDK.
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
Create **`eslint.config.js`** (NOT `.eslintrc.js`):
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const shopMinisConfig = require('@shopify/shop-minis-react/eslint/config')
|
|
11
|
+
|
|
12
|
+
module.exports = [shopMinisConfig]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**That's it!** TypeScript and JSX are supported out of the box.
|
|
16
|
+
|
|
17
|
+
**Important:**
|
|
18
|
+
- File must be named `eslint.config.js` (no dot, no "rc")
|
|
19
|
+
- This will lint all `.js`, `.jsx`, `.ts`, `.tsx` files in your project
|
|
20
|
+
- TypeScript and JSX parsing is configured automatically
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Check for errors
|
|
26
|
+
npx eslint .
|
|
27
|
+
|
|
28
|
+
# Auto-fix (converts <img> to <Image> AND adds import!)
|
|
29
|
+
npx eslint . --fix
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## What You Get
|
|
33
|
+
|
|
34
|
+
- ✅ No internal imports allowed
|
|
35
|
+
- ✅ Warnings for `<img>`, `<button>`, `<label>` tags (auto-fixes with imports)
|
|
36
|
+
- ✅ Manifest scope validation - ensures manifest.json has scopes for hooks you use (auto-fixes manifest)
|
|
37
|
+
|
|
38
|
+
## Rules
|
|
39
|
+
|
|
40
|
+
### `no-internal-imports`
|
|
41
|
+
|
|
42
|
+
Prevents importing from internal SDK directories.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// ❌ Error
|
|
46
|
+
import {something} from '@shopify/shop-minis-react/internal'
|
|
47
|
+
|
|
48
|
+
// ✅ Correct
|
|
49
|
+
import {Component} from '@shopify/shop-minis-react'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `prefer-sdk-components`
|
|
53
|
+
|
|
54
|
+
Suggests using SDK components instead of native HTML elements. **Fully auto-fixable** - fixes both tags and imports!
|
|
55
|
+
|
|
56
|
+
**Before:**
|
|
57
|
+
```tsx
|
|
58
|
+
const MyComponent = () => (
|
|
59
|
+
<img src="product.jpg" alt="Product" />
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**After running `npx eslint . --fix`:**
|
|
64
|
+
```tsx
|
|
65
|
+
import {Image} from '@shopify/shop-minis-react'
|
|
66
|
+
|
|
67
|
+
const MyComponent = () => (
|
|
68
|
+
<Image src="product.jpg" alt="Product" />
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Supported Components:**
|
|
73
|
+
- `<img>` → `<Image>`
|
|
74
|
+
- `<button>` → `<Button>`
|
|
75
|
+
- `<label>` → `<Label>`
|
|
76
|
+
|
|
77
|
+
**Auto-fix does TWO things:**
|
|
78
|
+
1. ✅ Replaces native element with SDK component
|
|
79
|
+
2. ✅ Adds import statement automatically (or adds to existing import)
|
|
80
|
+
|
|
81
|
+
### `validate-manifest`
|
|
82
|
+
|
|
83
|
+
Validates `src/manifest.json` configuration for scopes and permissions. **Auto-fixable** - adds missing values to manifest!
|
|
84
|
+
|
|
85
|
+
#### Scopes
|
|
86
|
+
|
|
87
|
+
Checks that hooks have required scopes:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// If you use this hook:
|
|
91
|
+
import {useCurrentUser} from '@shopify/shop-minis-react'
|
|
92
|
+
|
|
93
|
+
// Manifest must include:
|
|
94
|
+
{
|
|
95
|
+
"scopes": ["USER_SETTINGS_READ"]
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Scope Requirements:**
|
|
100
|
+
- `useCurrentUser` → `USER_SETTINGS_READ`
|
|
101
|
+
- `useSavedProducts` → `FAVORITES`
|
|
102
|
+
- `useOrders` → `ORDERS`
|
|
103
|
+
|
|
104
|
+
#### Permissions
|
|
105
|
+
|
|
106
|
+
Checks for native permission usage:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// If you use this hook:
|
|
110
|
+
import {useImagePicker} from '@shopify/shop-minis-react'
|
|
111
|
+
|
|
112
|
+
// Or browser APIs:
|
|
113
|
+
navigator.mediaDevices.getUserMedia({video: true})
|
|
114
|
+
|
|
115
|
+
// Manifest must include:
|
|
116
|
+
{
|
|
117
|
+
"permissions": ["CAMERA"]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Supported Permissions:**
|
|
122
|
+
- `CAMERA` - Required for `useImagePicker` hook or getUserMedia video
|
|
123
|
+
- `MICROPHONE` - Required for getUserMedia audio
|
|
124
|
+
- `MOTION` - Required for DeviceOrientation/DeviceMotion events
|
|
125
|
+
|
|
126
|
+
#### Trusted Domains
|
|
127
|
+
|
|
128
|
+
Checks that external URLs are in trusted_domains. Detects:
|
|
129
|
+
|
|
130
|
+
**Network Requests:**
|
|
131
|
+
- `fetch('https://api.example.com/data')`
|
|
132
|
+
- `new XMLHttpRequest().open('GET', 'https://...')`
|
|
133
|
+
- `new WebSocket('wss://api.example.com')`
|
|
134
|
+
- `new EventSource('https://api.example.com/events')`
|
|
135
|
+
- `navigator.sendBeacon('https://analytics.example.com')`
|
|
136
|
+
- `window.open('https://external.com')`
|
|
137
|
+
|
|
138
|
+
**Media & Resources:**
|
|
139
|
+
- `<img src="https://cdn.shopify.com/image.jpg" />`
|
|
140
|
+
- `<video src="https://videos.example.com/video.mp4" />`
|
|
141
|
+
- `<video poster="https://cdn.example.com/poster.jpg" />`
|
|
142
|
+
- `<audio src="https://audio.example.com/sound.mp3" />`
|
|
143
|
+
- `<source src="https://media.example.com/video.mp4" />`
|
|
144
|
+
- `<track src="https://cdn.example.com/captions.vtt" />`
|
|
145
|
+
- `<object data="https://cdn.example.com/file.pdf" />`
|
|
146
|
+
- `<embed src="https://cdn.example.com/file.swf" />`
|
|
147
|
+
- `<form action="https://api.example.com/submit" />`
|
|
148
|
+
|
|
149
|
+
**Note:** External scripts (`<script>`), stylesheets (`<link>`), and iframes are not supported and excluded from validation.
|
|
150
|
+
|
|
151
|
+
**Example manifest:**
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"trusted_domains": [
|
|
155
|
+
"api.example.com",
|
|
156
|
+
"cdn.shopify.com",
|
|
157
|
+
"videos.example.com"
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Wildcard support:**
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"trusted_domains": [
|
|
166
|
+
"*.shopify.com", // Matches any Shopify subdomain
|
|
167
|
+
"api.example.com", // Exact domain
|
|
168
|
+
"cdn.example.com/assets" // Specific path
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Auto-fix:**
|
|
174
|
+
```bash
|
|
175
|
+
npx eslint . --fix
|
|
176
|
+
# Automatically updates src/manifest.json
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Example errors:**
|
|
180
|
+
```
|
|
181
|
+
Hook "useCurrentUser" requires scope "USER_SETTINGS_READ" in src/manifest.json.
|
|
182
|
+
Hook "useImagePicker" requires permission "CAMERA" in src/manifest.json.
|
|
183
|
+
fetch() call loads from "api.example.com" which is not in trusted_domains.
|
|
184
|
+
<img> src attribute loads from "cdn.shopify.com" which is not in trusted_domains.
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Extending Rules
|
|
188
|
+
|
|
189
|
+
To add more component mappings to `prefer-sdk-components`, edit `eslint/rules/prefer-sdk-components.cjs`:
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
const defaultComponents = {
|
|
193
|
+
img: 'Image',
|
|
194
|
+
button: 'Button',
|
|
195
|
+
label: 'Label',
|
|
196
|
+
input: 'Input', // Add this
|
|
197
|
+
a: 'TransitionLink', // Add this
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
All consumers automatically get new rules - no config changes needed!
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* eslint-disable import/extensions */
|
|
2
|
+
/**
|
|
3
|
+
* ESLint config for projects using @shopify/shop-minis-react
|
|
4
|
+
* @fileoverview Recommended ESLint configuration for Shop Minis apps
|
|
5
|
+
*
|
|
6
|
+
* This config uses ESLint flat config format (supported in ESLint 8.57+)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Import the plugin directly so consumers don't need to install it separately
|
|
10
|
+
const shopMinisPlugin = require('./index.cjs')
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
|
|
14
|
+
languageOptions: {
|
|
15
|
+
parser: require('@typescript-eslint/parser'),
|
|
16
|
+
ecmaVersion: 'latest',
|
|
17
|
+
sourceType: 'module',
|
|
18
|
+
parserOptions: {
|
|
19
|
+
ecmaFeatures: {
|
|
20
|
+
jsx: true,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
plugins: {
|
|
25
|
+
'shop-minis': shopMinisPlugin,
|
|
26
|
+
},
|
|
27
|
+
rules: {
|
|
28
|
+
'shop-minis/no-internal-imports': 'error',
|
|
29
|
+
'shop-minis/prefer-sdk-components': 'warn',
|
|
30
|
+
'shop-minis/validate-manifest': 'error',
|
|
31
|
+
},
|
|
32
|
+
}
|
package/eslint/index.cjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* eslint-disable import/extensions */
|
|
2
|
+
/**
|
|
3
|
+
* ESLint plugin for @shopify/shop-minis-react
|
|
4
|
+
* @fileoverview Custom ESLint rules for Shop Minis React SDK
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const noInternalImports = require('./rules/no-internal-imports.cjs')
|
|
8
|
+
const preferSdkComponents = require('./rules/prefer-sdk-components.cjs')
|
|
9
|
+
const validateManifest = require('./rules/validate-manifest.cjs')
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
rules: {
|
|
13
|
+
'no-internal-imports': noInternalImports,
|
|
14
|
+
'prefer-sdk-components': preferSdkComponents,
|
|
15
|
+
'validate-manifest': validateManifest,
|
|
16
|
+
},
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to prevent importing from internal directories
|
|
3
|
+
* @fileoverview Disallow importing from @shopify/shop-minis-react/internal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
'Disallow importing from internal directories of @shopify/shop-minis-react',
|
|
12
|
+
category: 'Best Practices',
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
noInternalImports:
|
|
17
|
+
'Do not import from "{{importPath}}". Internal APIs are not part of the public API and may change without notice. Use the public exports from @shopify/shop-minis-react instead.',
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
create(context) {
|
|
23
|
+
return {
|
|
24
|
+
ImportDeclaration(node) {
|
|
25
|
+
const importPath = node.source.value
|
|
26
|
+
|
|
27
|
+
// Check if importing from internal directory
|
|
28
|
+
if (
|
|
29
|
+
importPath.includes('@shopify/shop-minis-react/internal') ||
|
|
30
|
+
importPath.includes('@shopify/shop-minis-react/src/internal')
|
|
31
|
+
) {
|
|
32
|
+
context.report({
|
|
33
|
+
node: node.source,
|
|
34
|
+
messageId: 'noInternalImports',
|
|
35
|
+
data: {
|
|
36
|
+
importPath,
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to prefer SDK components over native HTML elements
|
|
3
|
+
* @fileoverview Enforce using Shop Minis SDK components instead of native HTML
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
'Prefer Shop Minis SDK components over native HTML elements for better functionality and styling',
|
|
12
|
+
category: 'Best Practices',
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
fixable: 'code',
|
|
16
|
+
messages: {
|
|
17
|
+
preferSdkComponent:
|
|
18
|
+
'Use <{{sdkComponent}}> from @shopify/shop-minis-react instead of <{{nativeElement}}>. The SDK component provides optimized performance, consistent styling, and additional features.',
|
|
19
|
+
},
|
|
20
|
+
schema: [
|
|
21
|
+
{
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
components: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
description: 'Map of native elements to SDK components',
|
|
27
|
+
additionalProperties: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
create(context) {
|
|
38
|
+
// Default component mappings
|
|
39
|
+
const defaultComponents = {
|
|
40
|
+
img: 'Image',
|
|
41
|
+
button: 'Button',
|
|
42
|
+
label: 'Label',
|
|
43
|
+
// Future additions will go here:
|
|
44
|
+
// input: 'Input',
|
|
45
|
+
// a: 'Link',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get user configuration or use defaults
|
|
49
|
+
const options = context.options[0] || {}
|
|
50
|
+
const componentMap = {
|
|
51
|
+
...defaultComponents,
|
|
52
|
+
...(options.components || {}),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// eslint-disable-next-line @shopify/prefer-module-scope-constants
|
|
56
|
+
const SDK_PACKAGE = '@shopify/shop-minis-react'
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
JSXOpeningElement(node) {
|
|
60
|
+
const elementName = node.name.name
|
|
61
|
+
|
|
62
|
+
// Check if this is a native element we want to replace
|
|
63
|
+
if (componentMap[elementName]) {
|
|
64
|
+
const sdkComponent = componentMap[elementName]
|
|
65
|
+
|
|
66
|
+
context.report({
|
|
67
|
+
node,
|
|
68
|
+
messageId: 'preferSdkComponent',
|
|
69
|
+
data: {
|
|
70
|
+
nativeElement: elementName,
|
|
71
|
+
sdkComponent,
|
|
72
|
+
},
|
|
73
|
+
fix(fixer) {
|
|
74
|
+
const sourceCode = context.getSourceCode()
|
|
75
|
+
const openingElement = node
|
|
76
|
+
const jsxElement = node.parent
|
|
77
|
+
|
|
78
|
+
// Get the closing element if it exists
|
|
79
|
+
const closingElement = jsxElement.closingElement
|
|
80
|
+
|
|
81
|
+
const fixes = []
|
|
82
|
+
|
|
83
|
+
// Fix opening tag
|
|
84
|
+
const openingTagStart = openingElement.name.range[0]
|
|
85
|
+
const openingTagEnd = openingElement.name.range[1]
|
|
86
|
+
fixes.push(
|
|
87
|
+
fixer.replaceTextRange(
|
|
88
|
+
[openingTagStart, openingTagEnd],
|
|
89
|
+
sdkComponent
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// Fix closing tag if it exists (not self-closing)
|
|
94
|
+
if (closingElement) {
|
|
95
|
+
const closingTagStart = closingElement.name.range[0]
|
|
96
|
+
const closingTagEnd = closingElement.name.range[1]
|
|
97
|
+
fixes.push(
|
|
98
|
+
fixer.replaceTextRange(
|
|
99
|
+
[closingTagStart, closingTagEnd],
|
|
100
|
+
sdkComponent
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add import if it doesn't exist
|
|
106
|
+
const program = sourceCode.ast
|
|
107
|
+
const imports = program.body.filter(
|
|
108
|
+
importNode => importNode.type === 'ImportDeclaration'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// Check if we already import from the SDK
|
|
112
|
+
const sdkImport = imports.find(
|
|
113
|
+
importNode => importNode.source.value === SDK_PACKAGE
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if (sdkImport) {
|
|
117
|
+
// Check if this component is already imported
|
|
118
|
+
const hasComponent = sdkImport.specifiers.some(
|
|
119
|
+
spec =>
|
|
120
|
+
spec.type === 'ImportSpecifier' &&
|
|
121
|
+
spec.imported.name === sdkComponent
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (!hasComponent) {
|
|
125
|
+
// Add component to existing import
|
|
126
|
+
const lastSpecifier =
|
|
127
|
+
sdkImport.specifiers[sdkImport.specifiers.length - 1]
|
|
128
|
+
fixes.push(
|
|
129
|
+
fixer.insertTextAfter(lastSpecifier, `, ${sdkComponent}`)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// Add new import statement
|
|
134
|
+
const firstNode = program.body[0]
|
|
135
|
+
const importStatement = `import {${sdkComponent}} from '${SDK_PACKAGE}'\n`
|
|
136
|
+
|
|
137
|
+
if (firstNode) {
|
|
138
|
+
fixes.push(fixer.insertTextBefore(firstNode, importStatement))
|
|
139
|
+
} else {
|
|
140
|
+
fixes.push(
|
|
141
|
+
fixer.insertTextAfterRange([0, 0], importStatement)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return fixes
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
}
|