@shopify/shop-minis-react
Version:
React component library for Shopify Shop Minis with Tailwind CSS v4 support (source-only, requires TypeScript)
130 lines (116 loc) • 3.7 kB
JavaScript
/**
* ESLint rule to prefer SDK hooks over native browser APIs
* @fileoverview Enforce using Shop Minis SDK hooks instead of native browser APIs
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Prefer Shop Minis SDK hooks over native browser APIs for better compatibility and functionality',
category: 'Best Practices',
recommended: true,
},
messages: {
preferAsyncStorage:
'Use useAsyncStorage (or useSecureStorage for sensitive data) from @shopify/shop-minis-react instead of localStorage. The SDK hook provides async storage that works reliably in the Shop mini-app environment.',
},
schema: [
{
type: 'object',
properties: {
apis: {
type: 'object',
description: 'Map of native APIs to SDK hooks',
additionalProperties: {
type: 'string',
},
},
},
additionalProperties: false,
},
],
},
create(context) {
// Default API mappings
const defaultApis = {
localStorage: 'useAsyncStorage',
sessionStorage: 'useAsyncStorage',
}
// Get user configuration or use defaults
const options = context.options[0] || {}
const apiMap = {
...defaultApis,
...(options.apis || {}),
}
return {
MemberExpression(node) {
// Check for direct access: localStorage.getItem()
if (
node.object.type === 'Identifier' &&
Object.hasOwn(apiMap, node.object.name)
) {
const apiName = node.object.name
const sdkHook = apiMap[apiName]
context.report({
node: node.object,
messageId: 'preferAsyncStorage',
data: {
nativeApi: apiName,
sdkHook,
},
})
return
}
// Check for global access: window.localStorage or globalThis.localStorage
if (
node.object.type === 'MemberExpression' &&
node.object.object.type === 'Identifier' &&
(node.object.object.name === 'window' ||
node.object.object.name === 'globalThis') &&
node.object.property.type === 'Identifier' &&
Object.hasOwn(apiMap, node.object.property.name)
) {
const apiName = node.object.property.name
const sdkHook = apiMap[apiName]
context.report({
node: node.object,
messageId: 'preferAsyncStorage',
data: {
nativeApi: apiName,
sdkHook,
},
})
}
},
// Also catch direct references to localStorage/sessionStorage
Identifier(node) {
// Only flag if it's being used, not just referenced in imports
const parent = node.parent
// Skip if it's part of an import statement
if (
parent.type === 'ImportSpecifier' ||
parent.type === 'ImportDefaultSpecifier'
) {
return
}
// Skip if it's already part of a MemberExpression (handled above)
if (parent.type === 'MemberExpression' && parent.object === node) {
return
}
// Check if this is a direct reference to localStorage/sessionStorage
if (!Object.hasOwn(apiMap, node.name)) {
return
}
// Skip if it's the variable name being declared (e.g., const localStorage = ...)
if (parent.type === 'VariableDeclarator' && parent.id === node) {
return
}
context.report({
node,
messageId: 'preferAsyncStorage',
})
},
}
},
}