@shopify/shop-minis-react 0.1.7 → 0.2.0
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/components/atoms/image.js +35 -33
- package/dist/components/atoms/image.js.map +1 -1
- package/dist/hooks/navigation/useDeeplink.js +9 -9
- package/dist/hooks/navigation/useDeeplink.js.map +1 -1
- package/dist/providers/ImagePickerProvider.js +54 -55
- package/dist/providers/ImagePickerProvider.js.map +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 +10 -4
- package/src/components/atoms/image.test.tsx +41 -1
- package/src/components/atoms/image.tsx +8 -3
- package/src/hooks/navigation/useDeeplink.test.ts +429 -0
- package/src/hooks/navigation/useDeeplink.ts +6 -3
- package/src/providers/ImagePickerProvider.test.tsx +82 -155
- package/src/providers/ImagePickerProvider.tsx +21 -33
|
@@ -1,58 +1,60 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { memo as
|
|
3
|
-
import { cn as
|
|
4
|
-
import { getThumbhashDataURL as
|
|
5
|
-
const O =
|
|
1
|
+
import { jsx as u } from "react/jsx-runtime";
|
|
2
|
+
import { memo as j, useState as i, useEffect as k, useMemo as m, useCallback as y } from "react";
|
|
3
|
+
import { cn as d } from "../../lib/utils.js";
|
|
4
|
+
import { getThumbhashDataURL as I, getResizedImageUrl as z } from "../../utils/image.js";
|
|
5
|
+
const O = j(function(b) {
|
|
6
6
|
const {
|
|
7
7
|
src: r,
|
|
8
8
|
file: t,
|
|
9
|
-
thumbhash:
|
|
10
|
-
onLoad:
|
|
11
|
-
className:
|
|
12
|
-
style:
|
|
13
|
-
aspectRatio:
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
thumbhash: s,
|
|
10
|
+
onLoad: c,
|
|
11
|
+
className: f,
|
|
12
|
+
style: h,
|
|
13
|
+
aspectRatio: o = "auto",
|
|
14
|
+
objectFit: g = "contain",
|
|
15
|
+
...L
|
|
16
|
+
} = b, [p, U] = i(!1), [a, l] = i(null);
|
|
16
17
|
k(() => {
|
|
17
18
|
if (!t) {
|
|
18
|
-
|
|
19
|
+
l(null);
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
22
|
const e = URL.createObjectURL(t);
|
|
22
|
-
return
|
|
23
|
+
return l(e), () => {
|
|
23
24
|
URL.revokeObjectURL(e);
|
|
24
25
|
};
|
|
25
26
|
}, [t]);
|
|
26
|
-
const
|
|
27
|
-
() =>
|
|
28
|
-
[
|
|
29
|
-
),
|
|
27
|
+
const n = m(
|
|
28
|
+
() => I(s ?? void 0),
|
|
29
|
+
[s]
|
|
30
|
+
), R = y(
|
|
30
31
|
(e) => {
|
|
31
|
-
|
|
32
|
+
U(!0), c?.(e);
|
|
32
33
|
},
|
|
33
|
-
[
|
|
34
|
-
),
|
|
35
|
-
return /* @__PURE__ */
|
|
34
|
+
[c]
|
|
35
|
+
), v = m(() => a || z(r), [a, r]);
|
|
36
|
+
return /* @__PURE__ */ u(
|
|
36
37
|
"div",
|
|
37
38
|
{
|
|
38
|
-
className:
|
|
39
|
+
className: d("relative w-full", f),
|
|
39
40
|
style: {
|
|
40
|
-
...
|
|
41
|
-
aspectRatio:
|
|
42
|
-
backgroundImage:
|
|
41
|
+
...h,
|
|
42
|
+
...o !== "auto" && { aspectRatio: o },
|
|
43
|
+
backgroundImage: n ? `url(${n})` : void 0,
|
|
43
44
|
backgroundSize: "cover",
|
|
44
45
|
backgroundPosition: "center"
|
|
45
46
|
},
|
|
46
|
-
children: /* @__PURE__ */
|
|
47
|
+
children: /* @__PURE__ */ u(
|
|
47
48
|
"img",
|
|
48
49
|
{
|
|
49
|
-
className:
|
|
50
|
-
"absolute inset-0 opacity-0 size-full
|
|
51
|
-
|
|
50
|
+
className: d(
|
|
51
|
+
o === "auto" ? "opacity-0 w-full h-auto" : "absolute inset-0 opacity-0 size-full",
|
|
52
|
+
`object-${g}`,
|
|
53
|
+
p && "opacity-100"
|
|
52
54
|
),
|
|
53
|
-
src:
|
|
54
|
-
onLoad:
|
|
55
|
-
...
|
|
55
|
+
src: v,
|
|
56
|
+
onLoad: R,
|
|
57
|
+
...L
|
|
56
58
|
}
|
|
57
59
|
)
|
|
58
60
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","sources":["../../../src/components/atoms/image.tsx"],"sourcesContent":["/* eslint-disable jsx-a11y/alt-text */\n/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */\nimport {\n ImgHTMLAttributes,\n useCallback,\n useMemo,\n memo,\n useState,\n useEffect,\n} from 'react'\n\nimport {cn} from '../../lib/utils'\nimport {getThumbhashDataURL, getResizedImageUrl} from '../../utils'\n\ntype ImageProps = ImgHTMLAttributes<HTMLImageElement> & {\n src?: string\n file?: File\n thumbhash?: string | null\n aspectRatio?: number | string\n}\n\nexport const Image = memo(function Image(props: ImageProps) {\n const {\n src,\n file,\n thumbhash,\n onLoad,\n className,\n style,\n aspectRatio = 'auto',\n ...restProps\n } = props\n\n const [isLoaded, setIsLoaded] = useState(false)\n const [blobUrl, setBlobUrl] = useState<string | null>(null)\n\n // Create and manage blob URL for File objects\n useEffect(() => {\n if (!file) {\n setBlobUrl(null)\n return\n }\n\n const url = URL.createObjectURL(file)\n setBlobUrl(url)\n\n // Cleanup on unmount or when file changes\n return () => {\n URL.revokeObjectURL(url)\n }\n }, [file])\n\n const thumbhashDataURL = useMemo(\n () => getThumbhashDataURL(thumbhash ?? undefined),\n [thumbhash]\n )\n\n const handleLoad = useCallback(\n (event: React.SyntheticEvent<HTMLImageElement, Event>) => {\n setIsLoaded(true)\n onLoad?.(event)\n },\n [onLoad]\n )\n\n // Use blob URL if file is provided, otherwise use src\n const imageSrc = useMemo(() => {\n if (blobUrl) return blobUrl\n return getResizedImageUrl(src)\n }, [blobUrl, src])\n\n return (\n <div\n className={cn('relative w-full
|
|
1
|
+
{"version":3,"file":"image.js","sources":["../../../src/components/atoms/image.tsx"],"sourcesContent":["/* eslint-disable jsx-a11y/alt-text */\n/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */\nimport {\n ImgHTMLAttributes,\n useCallback,\n useMemo,\n memo,\n useState,\n useEffect,\n} from 'react'\n\nimport {cn} from '../../lib/utils'\nimport {getThumbhashDataURL, getResizedImageUrl} from '../../utils'\n\ntype ImageProps = ImgHTMLAttributes<HTMLImageElement> & {\n src?: string\n file?: File\n thumbhash?: string | null\n aspectRatio?: number | string\n objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none'\n}\n\nexport const Image = memo(function Image(props: ImageProps) {\n const {\n src,\n file,\n thumbhash,\n onLoad,\n className,\n style,\n aspectRatio = 'auto',\n objectFit = 'contain',\n ...restProps\n } = props\n\n const [isLoaded, setIsLoaded] = useState(false)\n const [blobUrl, setBlobUrl] = useState<string | null>(null)\n\n // Create and manage blob URL for File objects\n useEffect(() => {\n if (!file) {\n setBlobUrl(null)\n return\n }\n\n const url = URL.createObjectURL(file)\n setBlobUrl(url)\n\n // Cleanup on unmount or when file changes\n return () => {\n URL.revokeObjectURL(url)\n }\n }, [file])\n\n const thumbhashDataURL = useMemo(\n () => getThumbhashDataURL(thumbhash ?? undefined),\n [thumbhash]\n )\n\n const handleLoad = useCallback(\n (event: React.SyntheticEvent<HTMLImageElement, Event>) => {\n setIsLoaded(true)\n onLoad?.(event)\n },\n [onLoad]\n )\n\n // Use blob URL if file is provided, otherwise use src\n const imageSrc = useMemo(() => {\n if (blobUrl) return blobUrl\n return getResizedImageUrl(src)\n }, [blobUrl, src])\n\n return (\n <div\n className={cn('relative w-full', className)}\n style={{\n ...style,\n ...(aspectRatio !== 'auto' && {aspectRatio}),\n backgroundImage: thumbhashDataURL\n ? `url(${thumbhashDataURL})`\n : undefined,\n backgroundSize: 'cover',\n backgroundPosition: 'center',\n }}\n >\n <img\n className={cn(\n aspectRatio === 'auto'\n ? 'opacity-0 w-full h-auto'\n : 'absolute inset-0 opacity-0 size-full',\n `object-${objectFit}`,\n isLoaded && 'opacity-100'\n )}\n src={imageSrc}\n onLoad={handleLoad}\n {...restProps}\n />\n </div>\n )\n})\n"],"names":["Image","memo","props","src","file","thumbhash","onLoad","className","style","aspectRatio","objectFit","restProps","isLoaded","setIsLoaded","useState","blobUrl","setBlobUrl","useEffect","url","thumbhashDataURL","useMemo","getThumbhashDataURL","handleLoad","useCallback","event","imageSrc","getResizedImageUrl","jsx","cn"],"mappings":";;;;AAsBO,MAAMA,IAAQC,EAAK,SAAeC,GAAmB;AACpD,QAAA;AAAA,IACJ,KAAAC;AAAA,IACA,MAAAC;AAAA,IACA,WAAAC;AAAA,IACA,QAAAC;AAAA,IACA,WAAAC;AAAA,IACA,OAAAC;AAAA,IACA,aAAAC,IAAc;AAAA,IACd,WAAAC,IAAY;AAAA,IACZ,GAAGC;AAAA,EAAA,IACDT,GAEE,CAACU,GAAUC,CAAW,IAAIC,EAAS,EAAK,GACxC,CAACC,GAASC,CAAU,IAAIF,EAAwB,IAAI;AAG1D,EAAAG,EAAU,MAAM;AACd,QAAI,CAACb,GAAM;AACT,MAAAY,EAAW,IAAI;AACf;AAAA,IAAA;AAGI,UAAAE,IAAM,IAAI,gBAAgBd,CAAI;AACpC,WAAAY,EAAWE,CAAG,GAGP,MAAM;AACX,UAAI,gBAAgBA,CAAG;AAAA,IACzB;AAAA,EAAA,GACC,CAACd,CAAI,CAAC;AAET,QAAMe,IAAmBC;AAAA,IACvB,MAAMC,EAAoBhB,KAAa,MAAS;AAAA,IAChD,CAACA,CAAS;AAAA,EACZ,GAEMiB,IAAaC;AAAA,IACjB,CAACC,MAAyD;AACxD,MAAAX,EAAY,EAAI,GAChBP,IAASkB,CAAK;AAAA,IAChB;AAAA,IACA,CAAClB,CAAM;AAAA,EACT,GAGMmB,IAAWL,EAAQ,MACnBL,KACGW,EAAmBvB,CAAG,GAC5B,CAACY,GAASZ,CAAG,CAAC;AAGf,SAAA,gBAAAwB;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAWC,EAAG,mBAAmBrB,CAAS;AAAA,MAC1C,OAAO;AAAA,QACL,GAAGC;AAAA,QACH,GAAIC,MAAgB,UAAU,EAAC,aAAAA,EAAW;AAAA,QAC1C,iBAAiBU,IACb,OAAOA,CAAgB,MACvB;AAAA,QACJ,gBAAgB;AAAA,QAChB,oBAAoB;AAAA,MACtB;AAAA,MAEA,UAAA,gBAAAQ;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAWC;AAAA,YACTnB,MAAgB,SACZ,4BACA;AAAA,YACJ,UAAUC,CAAS;AAAA,YACnBE,KAAY;AAAA,UACd;AAAA,UACA,KAAKa;AAAA,UACL,QAAQH;AAAA,UACP,GAAGX;AAAA,QAAA;AAAA,MAAA;AAAA,IACN;AAAA,EACF;AAEJ,CAAC;"}
|
|
@@ -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,109 +1,108 @@
|
|
|
1
|
-
import { jsxs as
|
|
2
|
-
import { useRef as
|
|
3
|
-
import { useRequestPermissions as
|
|
4
|
-
const
|
|
5
|
-
function
|
|
6
|
-
const f =
|
|
1
|
+
import { jsxs as x, jsx as m } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as i, useCallback as s, useEffect as R, useMemo as I, createContext as b, useContext as A } from "react";
|
|
3
|
+
import { useRequestPermissions as L } from "../hooks/util/useRequestPermissions.js";
|
|
4
|
+
const k = b(null);
|
|
5
|
+
function H() {
|
|
6
|
+
const f = A(k);
|
|
7
7
|
if (!f)
|
|
8
8
|
throw new Error(
|
|
9
9
|
"useImagePickerContext must be used within an ImagePickerProvider"
|
|
10
10
|
);
|
|
11
11
|
return f;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const C = o(null), h = o(null), g = o(null), n = o(null), e = o(null), a = o(null), { requestPermission: d } = M(), t = s(() => {
|
|
13
|
+
function U({ children: f }) {
|
|
14
|
+
const C = i(null), h = i(null), g = i(null), n = i(null), e = i(null), a = i(null), { requestPermission: p } = L(), r = s(() => {
|
|
16
15
|
if (a.current) {
|
|
17
|
-
const { input:
|
|
18
|
-
|
|
16
|
+
const { input: u, handler: c } = a.current;
|
|
17
|
+
u.removeEventListener("cancel", c), a.current = null;
|
|
19
18
|
}
|
|
20
|
-
}, []),
|
|
19
|
+
}, []), o = s(() => {
|
|
21
20
|
e.current && (e.current(
|
|
22
21
|
new Error("New file picker opened before previous completed")
|
|
23
22
|
), n.current = null, e.current = null);
|
|
24
|
-
}, []),
|
|
25
|
-
(
|
|
26
|
-
const c =
|
|
27
|
-
c && n.current && (n.current(c), n.current = null, e.current = null,
|
|
23
|
+
}, []), d = s(
|
|
24
|
+
(u) => {
|
|
25
|
+
const c = u.target.files?.[0];
|
|
26
|
+
c && n.current && (n.current(c), n.current = null, e.current = null, r()), u.target.value = "";
|
|
28
27
|
},
|
|
29
|
-
[
|
|
30
|
-
),
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
28
|
+
[r]
|
|
29
|
+
), E = s(() => new Promise((u, c) => {
|
|
30
|
+
o(), r(), n.current = u, e.current = c;
|
|
31
|
+
const t = C.current;
|
|
32
|
+
if (!t) {
|
|
34
33
|
c(new Error("Gallery input not found")), n.current = null, e.current = null;
|
|
35
34
|
return;
|
|
36
35
|
}
|
|
37
|
-
const
|
|
38
|
-
e.current && (e.current(new Error("User cancelled file selection")), n.current = null, e.current = null),
|
|
36
|
+
const l = () => {
|
|
37
|
+
e.current && (e.current(new Error("User cancelled file selection")), n.current = null, e.current = null), r();
|
|
39
38
|
};
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
t.addEventListener("cancel", l), a.current = { input: t, handler: l }, p({ permission: "CAMERA" }).then(() => {
|
|
40
|
+
t.click();
|
|
42
41
|
}).catch(() => {
|
|
43
|
-
|
|
44
|
-
})
|
|
45
|
-
}), [
|
|
46
|
-
(
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
|
|
42
|
+
t.click();
|
|
43
|
+
});
|
|
44
|
+
}), [o, r, p]), P = s(
|
|
45
|
+
(u = "back") => new Promise((c, t) => {
|
|
46
|
+
o(), r(), n.current = c, e.current = t;
|
|
47
|
+
const l = u === "front" ? h.current : g.current;
|
|
48
|
+
if (!l) {
|
|
49
|
+
t(new Error("Camera input not found")), n.current = null, e.current = null;
|
|
51
50
|
return;
|
|
52
51
|
}
|
|
53
|
-
const
|
|
54
|
-
e.current && (e.current(new Error("User cancelled camera")), n.current = null, e.current = null),
|
|
52
|
+
const w = () => {
|
|
53
|
+
e.current && (e.current(new Error("User cancelled camera")), n.current = null, e.current = null), r();
|
|
55
54
|
};
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
l.addEventListener("cancel", w), a.current = { input: l, handler: w }, p({ permission: "CAMERA" }).then(({ granted: y }) => {
|
|
56
|
+
y ? l.click() : (t(new Error("Camera permission not granted")), n.current = null, e.current = null);
|
|
58
57
|
}).catch(() => {
|
|
59
|
-
|
|
60
|
-
})
|
|
58
|
+
t(new Error("Camera permission not granted")), n.current = null, e.current = null;
|
|
59
|
+
});
|
|
61
60
|
}),
|
|
62
|
-
[
|
|
61
|
+
[o, r, p]
|
|
63
62
|
);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}, [
|
|
67
|
-
const
|
|
63
|
+
R(() => () => {
|
|
64
|
+
o(), r();
|
|
65
|
+
}, [o, r]);
|
|
66
|
+
const v = I(
|
|
68
67
|
() => ({
|
|
69
68
|
openCamera: P,
|
|
70
|
-
openGallery:
|
|
69
|
+
openGallery: E
|
|
71
70
|
}),
|
|
72
|
-
[P,
|
|
71
|
+
[P, E]
|
|
73
72
|
);
|
|
74
|
-
return /* @__PURE__ */
|
|
73
|
+
return /* @__PURE__ */ x(k.Provider, { value: v, children: [
|
|
75
74
|
f,
|
|
76
|
-
/* @__PURE__ */
|
|
75
|
+
/* @__PURE__ */ m(
|
|
77
76
|
"input",
|
|
78
77
|
{
|
|
79
78
|
ref: C,
|
|
80
79
|
type: "file",
|
|
81
80
|
accept: "image/*",
|
|
82
|
-
onChange:
|
|
81
|
+
onChange: d,
|
|
83
82
|
style: { display: "none" },
|
|
84
83
|
"aria-hidden": "true"
|
|
85
84
|
}
|
|
86
85
|
),
|
|
87
|
-
/* @__PURE__ */
|
|
86
|
+
/* @__PURE__ */ m(
|
|
88
87
|
"input",
|
|
89
88
|
{
|
|
90
89
|
ref: h,
|
|
91
90
|
type: "file",
|
|
92
91
|
accept: "image/*",
|
|
93
92
|
capture: "user",
|
|
94
|
-
onChange:
|
|
93
|
+
onChange: d,
|
|
95
94
|
style: { display: "none" },
|
|
96
95
|
"aria-hidden": "true"
|
|
97
96
|
}
|
|
98
97
|
),
|
|
99
|
-
/* @__PURE__ */
|
|
98
|
+
/* @__PURE__ */ m(
|
|
100
99
|
"input",
|
|
101
100
|
{
|
|
102
101
|
ref: g,
|
|
103
102
|
type: "file",
|
|
104
103
|
accept: "image/*",
|
|
105
104
|
capture: "environment",
|
|
106
|
-
onChange:
|
|
105
|
+
onChange: d,
|
|
107
106
|
style: { display: "none" },
|
|
108
107
|
"aria-hidden": "true"
|
|
109
108
|
}
|
|
@@ -111,7 +110,7 @@ function F({ children: f }) {
|
|
|
111
110
|
] });
|
|
112
111
|
}
|
|
113
112
|
export {
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
U as ImagePickerProvider,
|
|
114
|
+
H as useImagePickerContext
|
|
116
115
|
};
|
|
117
116
|
//# sourceMappingURL=ImagePickerProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImagePickerProvider.js","sources":["../../src/providers/ImagePickerProvider.tsx"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useRef,\n useCallback,\n useEffect,\n useMemo,\n} from 'react'\n\nimport {useRequestPermissions} from '../hooks/util/useRequestPermissions'\n\nexport type CameraFacing = 'front' | 'back'\n\ninterface ImagePickerContextValue {\n openCamera: (cameraFacing?: CameraFacing) => Promise<File>\n openGallery: () => Promise<File>\n}\n\nconst ImagePickerContext = createContext<ImagePickerContextValue | null>(null)\n\nexport function useImagePickerContext() {\n const context = useContext(ImagePickerContext)\n if (!context) {\n throw new Error(\n 'useImagePickerContext must be used within an ImagePickerProvider'\n )\n }\n return context\n}\n\ninterface ImagePickerProviderProps {\n children: React.ReactNode\n}\n\nconst isAndroid = () => window?.minisParams?.platform === 'android'\n\nexport function ImagePickerProvider({children}: ImagePickerProviderProps) {\n const galleryInputRef = useRef<HTMLInputElement>(null)\n const frontCameraInputRef = useRef<HTMLInputElement>(null)\n const backCameraInputRef = useRef<HTMLInputElement>(null)\n const resolveRef = useRef<((file: File) => void) | null>(null)\n const rejectRef = useRef<((reason: Error) => void) | null>(null)\n const activeCancelHandlerRef = useRef<{\n input: HTMLInputElement\n handler: () => void\n } | null>(null)\n\n const {requestPermission} = useRequestPermissions()\n\n const cleanupCancelHandler = useCallback(() => {\n if (activeCancelHandlerRef.current) {\n const {input, handler} = activeCancelHandlerRef.current\n input.removeEventListener('cancel', handler)\n activeCancelHandlerRef.current = null\n }\n }, [])\n\n const rejectPendingPromise = useCallback(() => {\n if (rejectRef.current) {\n rejectRef.current(\n new Error('New file picker opened before previous completed')\n )\n resolveRef.current = null\n rejectRef.current = null\n }\n }, [])\n\n const handleFileChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0]\n\n if (file && resolveRef.current) {\n resolveRef.current(file)\n\n resolveRef.current = null\n rejectRef.current = null\n\n cleanupCancelHandler()\n }\n\n event.target.value = ''\n },\n [cleanupCancelHandler]\n )\n\n const openGallery = useCallback(() => {\n return new Promise<File>((resolve, reject) => {\n rejectPendingPromise()\n cleanupCancelHandler()\n\n resolveRef.current = resolve\n rejectRef.current = reject\n\n const input = galleryInputRef.current\n\n if (!input) {\n reject(new Error('Gallery input not found'))\n resolveRef.current = null\n rejectRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n rejectRef.current(new Error('User cancelled file selection'))\n resolveRef.current = null\n rejectRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n if (isAndroid()) {\n // Android requires explicit camera permission for camera picker\n requestPermission({permission: 'CAMERA'})\n .then(() => {\n // This will show both Camera and Gallery\n input.click()\n })\n .catch(() => {\n // Show only Gallery\n input.click()\n })\n } else {\n input.click()\n }\n })\n }, [rejectPendingPromise, cleanupCancelHandler, requestPermission])\n\n const openCamera = useCallback(\n (cameraFacing: CameraFacing = 'back') => {\n return new Promise<File>((resolve, reject) => {\n rejectPendingPromise()\n cleanupCancelHandler()\n\n resolveRef.current = resolve\n rejectRef.current = reject\n\n const input =\n cameraFacing === 'front'\n ? frontCameraInputRef.current\n : backCameraInputRef.current\n\n if (!input) {\n reject(new Error('Camera input not found'))\n resolveRef.current = null\n rejectRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n rejectRef.current(new Error('User cancelled camera'))\n resolveRef.current = null\n rejectRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n if (isAndroid()) {\n // Android requires explicit camera permission\n requestPermission({permission: 'CAMERA'})\n .then(({granted}) => {\n if (granted) {\n input.click()\n } else {\n reject(new Error('Camera permission not granted'))\n resolveRef.current = null\n rejectRef.current = null\n }\n })\n .catch(() => {\n reject(new Error('Camera permission not granted'))\n resolveRef.current = null\n rejectRef.current = null\n })\n } else {\n input.click()\n }\n })\n },\n [rejectPendingPromise, cleanupCancelHandler, requestPermission]\n )\n\n useEffect(() => {\n return () => {\n rejectPendingPromise()\n cleanupCancelHandler()\n }\n }, [rejectPendingPromise, cleanupCancelHandler])\n\n const contextValue: ImagePickerContextValue = useMemo(\n () => ({\n openCamera,\n openGallery,\n }),\n [openCamera, openGallery]\n )\n\n return (\n <ImagePickerContext.Provider value={contextValue}>\n {children}\n <input\n ref={galleryInputRef}\n type=\"file\"\n accept=\"image/*\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n <input\n ref={frontCameraInputRef}\n type=\"file\"\n accept=\"image/*\"\n capture=\"user\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n <input\n ref={backCameraInputRef}\n type=\"file\"\n accept=\"image/*\"\n capture=\"environment\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n </ImagePickerContext.Provider>\n )\n}\n"],"names":["ImagePickerContext","createContext","useImagePickerContext","context","useContext","isAndroid","ImagePickerProvider","children","galleryInputRef","useRef","frontCameraInputRef","backCameraInputRef","resolveRef","rejectRef","activeCancelHandlerRef","requestPermission","useRequestPermissions","cleanupCancelHandler","useCallback","input","handler","rejectPendingPromise","handleFileChange","event","file","openGallery","resolve","reject","handleCancel","openCamera","cameraFacing","granted","useEffect","contextValue","useMemo","jsxs","jsx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ImagePickerProvider.js","sources":["../../src/providers/ImagePickerProvider.tsx"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useRef,\n useCallback,\n useEffect,\n useMemo,\n} from 'react'\n\nimport {useRequestPermissions} from '../hooks/util/useRequestPermissions'\n\nexport type CameraFacing = 'front' | 'back'\n\ninterface ImagePickerContextValue {\n openCamera: (cameraFacing?: CameraFacing) => Promise<File>\n openGallery: () => Promise<File>\n}\n\nconst ImagePickerContext = createContext<ImagePickerContextValue | null>(null)\n\nexport function useImagePickerContext() {\n const context = useContext(ImagePickerContext)\n if (!context) {\n throw new Error(\n 'useImagePickerContext must be used within an ImagePickerProvider'\n )\n }\n return context\n}\n\ninterface ImagePickerProviderProps {\n children: React.ReactNode\n}\n\nexport function ImagePickerProvider({children}: ImagePickerProviderProps) {\n const galleryInputRef = useRef<HTMLInputElement>(null)\n const frontCameraInputRef = useRef<HTMLInputElement>(null)\n const backCameraInputRef = useRef<HTMLInputElement>(null)\n const resolveRef = useRef<((file: File) => void) | null>(null)\n const rejectRef = useRef<((reason: Error) => void) | null>(null)\n const activeCancelHandlerRef = useRef<{\n input: HTMLInputElement\n handler: () => void\n } | null>(null)\n\n const {requestPermission} = useRequestPermissions()\n\n const cleanupCancelHandler = useCallback(() => {\n if (activeCancelHandlerRef.current) {\n const {input, handler} = activeCancelHandlerRef.current\n input.removeEventListener('cancel', handler)\n activeCancelHandlerRef.current = null\n }\n }, [])\n\n const rejectPendingPromise = useCallback(() => {\n if (rejectRef.current) {\n rejectRef.current(\n new Error('New file picker opened before previous completed')\n )\n resolveRef.current = null\n rejectRef.current = null\n }\n }, [])\n\n const handleFileChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0]\n\n if (file && resolveRef.current) {\n resolveRef.current(file)\n\n resolveRef.current = null\n rejectRef.current = null\n\n cleanupCancelHandler()\n }\n\n event.target.value = ''\n },\n [cleanupCancelHandler]\n )\n\n const openGallery = useCallback(() => {\n return new Promise<File>((resolve, reject) => {\n rejectPendingPromise()\n cleanupCancelHandler()\n\n resolveRef.current = resolve\n rejectRef.current = reject\n\n const input = galleryInputRef.current\n\n if (!input) {\n reject(new Error('Gallery input not found'))\n resolveRef.current = null\n rejectRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n rejectRef.current(new Error('User cancelled file selection'))\n resolveRef.current = null\n rejectRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n requestPermission({permission: 'CAMERA'})\n .then(() => {\n // This will show both Camera and Gallery\n input.click()\n })\n .catch(() => {\n // Show only Gallery\n input.click()\n })\n })\n }, [rejectPendingPromise, cleanupCancelHandler, requestPermission])\n\n const openCamera = useCallback(\n (cameraFacing: CameraFacing = 'back') => {\n return new Promise<File>((resolve, reject) => {\n rejectPendingPromise()\n cleanupCancelHandler()\n\n resolveRef.current = resolve\n rejectRef.current = reject\n\n const input =\n cameraFacing === 'front'\n ? frontCameraInputRef.current\n : backCameraInputRef.current\n\n if (!input) {\n reject(new Error('Camera input not found'))\n resolveRef.current = null\n rejectRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n rejectRef.current(new Error('User cancelled camera'))\n resolveRef.current = null\n rejectRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n requestPermission({permission: 'CAMERA'})\n .then(({granted}) => {\n if (granted) {\n input.click()\n } else {\n reject(new Error('Camera permission not granted'))\n resolveRef.current = null\n rejectRef.current = null\n }\n })\n .catch(() => {\n reject(new Error('Camera permission not granted'))\n resolveRef.current = null\n rejectRef.current = null\n })\n })\n },\n [rejectPendingPromise, cleanupCancelHandler, requestPermission]\n )\n\n useEffect(() => {\n return () => {\n rejectPendingPromise()\n cleanupCancelHandler()\n }\n }, [rejectPendingPromise, cleanupCancelHandler])\n\n const contextValue: ImagePickerContextValue = useMemo(\n () => ({\n openCamera,\n openGallery,\n }),\n [openCamera, openGallery]\n )\n\n return (\n <ImagePickerContext.Provider value={contextValue}>\n {children}\n <input\n ref={galleryInputRef}\n type=\"file\"\n accept=\"image/*\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n <input\n ref={frontCameraInputRef}\n type=\"file\"\n accept=\"image/*\"\n capture=\"user\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n <input\n ref={backCameraInputRef}\n type=\"file\"\n accept=\"image/*\"\n capture=\"environment\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n </ImagePickerContext.Provider>\n )\n}\n"],"names":["ImagePickerContext","createContext","useImagePickerContext","context","useContext","ImagePickerProvider","children","galleryInputRef","useRef","frontCameraInputRef","backCameraInputRef","resolveRef","rejectRef","activeCancelHandlerRef","requestPermission","useRequestPermissions","cleanupCancelHandler","useCallback","input","handler","rejectPendingPromise","handleFileChange","event","file","openGallery","resolve","reject","handleCancel","openCamera","cameraFacing","granted","useEffect","contextValue","useMemo","jsxs","jsx"],"mappings":"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"}
|
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
|
+
}
|