@shopify/shop-minis-react 0.4.4 → 0.4.6

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.
@@ -1,5 +1,5 @@
1
- var e = { exports: {} };
1
+ var r = {};
2
2
  export {
3
- e as __module
3
+ r as __exports
4
4
  };
5
5
  //# sourceMappingURL=index4.js.map
@@ -1,6 +1,5 @@
1
- import { __require as r } from "../shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js";
2
- var i = r();
1
+ var e = { exports: {} };
3
2
  export {
4
- i as l
3
+ e as __module
5
4
  };
6
5
  //# sourceMappingURL=index5.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index5.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
1
+ {"version":3,"file":"index5.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -1,5 +1,6 @@
1
- var r = {};
1
+ import { __require as r } from "../shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js";
2
+ var i = r();
2
3
  export {
3
- r as __exports
4
+ i as l
4
5
  };
5
6
  //# sourceMappingURL=index6.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index6.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
1
+ {"version":3,"file":"index6.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -1,4 +1,4 @@
1
- import { __module as q } from "../../../../../../../../_virtual/index4.js";
1
+ import { __module as q } from "../../../../../../../../_virtual/index5.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/index5.js";
5
+ import { l as Q } from "../../../../../../../_virtual/index6.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];
@@ -1,4 +1,4 @@
1
- import { __exports as i } from "../../../../../../_virtual/index6.js";
1
+ import { __exports as i } from "../../../../../../_virtual/index4.js";
2
2
  var c;
3
3
  function d() {
4
4
  if (c) return i;
package/eslint/config.cjs CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  // Import the plugin directly so consumers don't need to install it separately
10
+ const compatPlugin = require('eslint-plugin-compat')
10
11
  const reactPlugin = require('eslint-plugin-react')
11
12
 
12
13
  const shopMinisPlugin = require('./index.cjs')
@@ -26,6 +27,7 @@ module.exports = {
26
27
  plugins: {
27
28
  'shop-minis': shopMinisPlugin,
28
29
  react: reactPlugin,
30
+ compat: compatPlugin,
29
31
  },
30
32
  rules: {
31
33
  // Console usage
@@ -35,7 +37,9 @@ module.exports = {
35
37
  'react/no-danger': 'error',
36
38
 
37
39
  // Shop Minis custom rules
40
+ 'shop-minis/no-env-without-fallback': 'error',
38
41
  'shop-minis/no-internal-imports': 'error',
42
+ 'shop-minis/no-secrets': 'error',
39
43
  'shop-minis/prefer-sdk-components': 'warn',
40
44
  'shop-minis/prefer-sdk-hooks': 'warn',
41
45
  'shop-minis/validate-manifest': 'error',
@@ -68,5 +72,6 @@ module.exports = {
68
72
  'WebAssembly is not supported in the Shop Minis environment. Consider using alternative JavaScript implementations.',
69
73
  },
70
74
  ],
75
+ 'compat/compat': 'error',
71
76
  },
72
77
  }
package/eslint/index.cjs CHANGED
@@ -4,14 +4,18 @@
4
4
  * @fileoverview Custom ESLint rules for Shop Minis React SDK
5
5
  */
6
6
 
7
+ const noEnvWithoutFallback = require('./rules/no-env-without-fallback.cjs')
7
8
  const noInternalImports = require('./rules/no-internal-imports.cjs')
9
+ const noSecrets = require('./rules/no-secrets.cjs')
8
10
  const preferSdkComponents = require('./rules/prefer-sdk-components.cjs')
9
11
  const preferSdkHooks = require('./rules/prefer-sdk-hooks.cjs')
10
12
  const validateManifest = require('./rules/validate-manifest.cjs')
11
13
 
12
14
  module.exports = {
13
15
  rules: {
16
+ 'no-env-without-fallback': noEnvWithoutFallback,
14
17
  'no-internal-imports': noInternalImports,
18
+ 'no-secrets': noSecrets,
15
19
  'prefer-sdk-components': preferSdkComponents,
16
20
  'prefer-sdk-hooks': preferSdkHooks,
17
21
  'validate-manifest': validateManifest,
@@ -0,0 +1,148 @@
1
+ /**
2
+ * ESLint rule to require fallbacks for environment variables
3
+ * @fileoverview Disallow using import.meta.env without a fallback value
4
+ *
5
+ * In Shop Minis, environment variables work in development but are not available
6
+ * in production. This rule ensures developers always provide fallback values.
7
+ */
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description:
14
+ 'Require fallback values when using import.meta.env variables',
15
+ category: 'Best Practices',
16
+ recommended: true,
17
+ },
18
+ messages: {
19
+ noFallback:
20
+ 'Environment variable "{{ name }}" must have a fallback value. Environment variables are only available in development. Use || or ?? to provide a production fallback.',
21
+ },
22
+ schema: [],
23
+ },
24
+
25
+ create(context) {
26
+ return {
27
+ MemberExpression(node) {
28
+ // Check if this is import.meta.env.SOMETHING
29
+ if (!isImportMetaEnv(node)) {
30
+ return
31
+ }
32
+
33
+ // Get the env variable name for the error message
34
+ const envName = node.property.name || node.property.value || 'unknown'
35
+
36
+ // Check if it has a fallback (|| or ??)
37
+ if (hasFallback(node)) {
38
+ return
39
+ }
40
+
41
+ context.report({
42
+ node,
43
+ messageId: 'noFallback',
44
+ data: {
45
+ name: `import.meta.env.${envName}`,
46
+ },
47
+ })
48
+ },
49
+ }
50
+ },
51
+ }
52
+
53
+ /**
54
+ * Check if node is import.meta.env.SOMETHING
55
+ */
56
+ function isImportMetaEnv(node) {
57
+ // Must be a member expression with a property
58
+ if (node.type !== 'MemberExpression' || !node.property) {
59
+ return false
60
+ }
61
+
62
+ // The object should be import.meta.env
63
+ const obj = node.object
64
+ if (
65
+ obj.type === 'MemberExpression' &&
66
+ obj.object.type === 'MetaProperty' &&
67
+ obj.object.meta.name === 'import' &&
68
+ obj.object.property.name === 'meta' &&
69
+ obj.property.name === 'env'
70
+ ) {
71
+ return true
72
+ }
73
+
74
+ return false
75
+ }
76
+
77
+ /**
78
+ * Check if the node has a fallback via || or ?? operator
79
+ */
80
+ function hasFallback(node) {
81
+ let current = node.parent
82
+
83
+ // Walk up the tree to find if we're in a logical expression with fallback
84
+ while (current) {
85
+ // Direct fallback: import.meta.env.FOO || 'default' or import.meta.env.FOO ?? 'default'
86
+ // Also handles chains: import.meta.env.A || import.meta.env.B || 'default'
87
+ if (
88
+ current.type === 'LogicalExpression' &&
89
+ (current.operator === '||' || current.operator === '??')
90
+ ) {
91
+ // Check if this logical expression chain eventually has a non-env fallback
92
+ if (logicalChainHasFallback(current)) {
93
+ return true
94
+ }
95
+ }
96
+
97
+ // Conditional expression: import.meta.env.FOO ? import.meta.env.FOO : 'default'
98
+ if (current.type === 'ConditionalExpression') {
99
+ // If used in the test part, it has a fallback (the alternate)
100
+ if (node === current.test) {
101
+ return true
102
+ }
103
+ // If used in the consequent and test is an env var check, it's valid
104
+ if (node === current.consequent && isImportMetaEnv(current.test)) {
105
+ return true
106
+ }
107
+ }
108
+
109
+ current = current.parent
110
+ }
111
+
112
+ return false
113
+ }
114
+
115
+ /**
116
+ * Check if a logical expression chain has a fallback at the end
117
+ * For: A || B || C, we walk up to find the topmost || or ?? and check its right side
118
+ */
119
+ function logicalChainHasFallback(node) {
120
+ // Walk up to the topmost logical expression in the chain
121
+ let topmost = node
122
+ while (
123
+ topmost.parent &&
124
+ topmost.parent.type === 'LogicalExpression' &&
125
+ (topmost.parent.operator === '||' || topmost.parent.operator === '??') &&
126
+ topmost.parent.left === topmost
127
+ ) {
128
+ topmost = topmost.parent
129
+ }
130
+
131
+ // The rightmost value in the chain is the fallback
132
+ // Check that it's not also an import.meta.env (that would mean no real fallback)
133
+ const rightmost = getRightmostValue(topmost)
134
+ return !isImportMetaEnv(rightmost)
135
+ }
136
+
137
+ /**
138
+ * Get the rightmost value in a logical expression chain
139
+ */
140
+ function getRightmostValue(node) {
141
+ if (
142
+ node.type === 'LogicalExpression' &&
143
+ (node.operator === '||' || node.operator === '??')
144
+ ) {
145
+ return getRightmostValue(node.right)
146
+ }
147
+ return node
148
+ }
@@ -0,0 +1,27 @@
1
+ /* eslint-disable import/extensions */
2
+ /**
3
+ * ESLint rule wrapper for eslint-plugin-no-secrets with custom messages
4
+ * @fileoverview Customized messages for detecting hardcoded secrets in Shop Minis
5
+ */
6
+
7
+ const noSecretsPlugin = require('eslint-plugin-no-secrets')
8
+
9
+ // Get the original rule
10
+ const originalRule = noSecretsPlugin.rules['no-secrets']
11
+
12
+ module.exports = {
13
+ meta: {
14
+ ...originalRule.meta,
15
+ messages: {
16
+ HIGH_ENTROPY:
17
+ 'This string may be a hardcoded credential, which should never be committed. Please check the Shop Minis documentation learn how to handle these cases. You can disable this rule if it is not a credential.',
18
+ PATTERN_MATCH:
19
+ 'Potential {{ name }} detected. Hardcoded credentials should never be committed. Please check the Shop Minis documentation learn how to handle these cases. You can disable this rule if it is not a credential.',
20
+ },
21
+ },
22
+ create(context) {
23
+ // Create the original rule's visitor
24
+ const originalVisitor = originalRule.create(context)
25
+ return originalVisitor
26
+ },
27
+ }
@@ -221,32 +221,84 @@ module.exports = {
221
221
  },
222
222
 
223
223
  // Detect browser API usage for permissions
224
- MemberExpression(node) {
225
- const sourceCode = context.getSourceCode()
226
- const code = sourceCode.getText(node)
224
+ MemberExpression() {
225
+ // This visitor is kept for potential future permission detection
226
+ // getUserMedia detection has been moved to CallExpression for better constraint analysis
227
+ },
227
228
 
228
- // Check for camera/microphone API
229
+ // Detect network requests and event listeners
230
+ CallExpression(node) {
231
+ // Check for getUserMedia calls
229
232
  if (
230
- code.includes('navigator.mediaDevices.getUserMedia') ||
231
- code.includes('navigator.getUserMedia')
233
+ node.callee.type === 'MemberExpression' &&
234
+ node.callee.property.name === 'getUserMedia'
232
235
  ) {
233
- // Need to check the constraints to determine if it's camera or microphone
234
- // For now, we'll flag both as potentially needed
235
- requiredPermissions.add({
236
- permission: 'CAMERA',
237
- reason: 'getUserMedia API usage',
238
- node,
239
- })
240
- requiredPermissions.add({
241
- permission: 'MICROPHONE',
242
- reason: 'getUserMedia API usage',
243
- node,
244
- })
236
+ // Validate it's actually navigator.getUserMedia or navigator.mediaDevices.getUserMedia
237
+ const sourceCode = context.getSourceCode()
238
+ const calleeText = sourceCode.getText(node.callee)
239
+
240
+ if (
241
+ calleeText.includes('navigator.mediaDevices.getUserMedia') ||
242
+ calleeText.includes('navigator.getUserMedia')
243
+ ) {
244
+ // Get the constraints argument (first argument)
245
+ const constraints = node.arguments[0]
246
+
247
+ let needsCamera = true
248
+ let needsMicrophone = true
249
+
250
+ if (constraints && constraints.type === 'ObjectExpression') {
251
+ // Check if video is explicitly set to false
252
+ const videoProp = constraints.properties.find(
253
+ prop =>
254
+ prop.type === 'Property' &&
255
+ prop.key &&
256
+ (prop.key.name === 'video' ||
257
+ (prop.key.type === 'Literal' && prop.key.value === 'video'))
258
+ )
259
+ if (
260
+ videoProp &&
261
+ videoProp.value.type === 'Literal' &&
262
+ videoProp.value.value === false
263
+ ) {
264
+ needsCamera = false
265
+ }
266
+
267
+ // Check if audio is explicitly set to false
268
+ const audioProp = constraints.properties.find(
269
+ prop =>
270
+ prop.type === 'Property' &&
271
+ prop.key &&
272
+ (prop.key.name === 'audio' ||
273
+ (prop.key.type === 'Literal' && prop.key.value === 'audio'))
274
+ )
275
+ if (
276
+ audioProp &&
277
+ audioProp.value.type === 'Literal' &&
278
+ audioProp.value.value === false
279
+ ) {
280
+ needsMicrophone = false
281
+ }
282
+ }
283
+
284
+ if (needsCamera) {
285
+ requiredPermissions.add({
286
+ permission: 'CAMERA',
287
+ reason: 'getUserMedia API usage with video',
288
+ node,
289
+ })
290
+ }
291
+
292
+ if (needsMicrophone) {
293
+ requiredPermissions.add({
294
+ permission: 'MICROPHONE',
295
+ reason: 'getUserMedia API usage with audio',
296
+ node,
297
+ })
298
+ }
299
+ }
245
300
  }
246
- },
247
301
 
248
- // Detect network requests and event listeners
249
- CallExpression(node) {
250
302
  if (!node.arguments[0]) {
251
303
  return
252
304
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shopify/shop-minis-react",
3
3
  "license": "SEE LICENSE IN LICENSE.txt",
4
- "version": "0.4.4",
4
+ "version": "0.4.6",
5
5
  "sideEffects": false,
6
6
  "type": "module",
7
7
  "engines": {
@@ -50,14 +50,16 @@
50
50
  "@types/lodash": "4.17.20",
51
51
  "@types/react-window": "1.8.8",
52
52
  "@types/url-parse": "1.4.9",
53
- "@typescript-eslint/parser": "^7.0.0",
53
+ "@typescript-eslint/parser": "7.0.0",
54
54
  "@vitejs/plugin-react": "4.5.1",
55
55
  "class-variance-authority": "0.7.1",
56
56
  "clsx": "2.1.1",
57
57
  "color": "4.2.3",
58
58
  "embla-carousel-react": "8.6.0",
59
- "eslint": "^8.57.0",
60
- "eslint-plugin-react": "^7.37.5",
59
+ "eslint": "8.57.0",
60
+ "eslint-plugin-compat": "6.0.2",
61
+ "eslint-plugin-no-secrets": "2.2.1",
62
+ "eslint-plugin-react": "7.37.5",
61
63
  "js-base64": "3.7.7",
62
64
  "lodash": "4.17.21",
63
65
  "lucide-react": "0.513.0",