@shopify/shop-minis-react
Version:
React component library for Shopify Shop Minis with Tailwind CSS v4 support (source-only, requires TypeScript)
149 lines (132 loc) • 4.13 kB
JavaScript
/**
* ESLint rule to require fallbacks for environment variables
* @fileoverview Disallow using import.meta.env without a fallback value
*
* In Shop Minis, environment variables work in development but are not available
* in production. This rule ensures developers always provide fallback values.
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Require fallback values when using import.meta.env variables',
category: 'Best Practices',
recommended: true,
},
messages: {
noFallback:
'Environment variable "{{ name }}" must have a fallback value. Environment variables are only available in development. Use || or ?? to provide a production fallback.',
},
schema: [],
},
create(context) {
return {
MemberExpression(node) {
// Check if this is import.meta.env.SOMETHING
if (!isImportMetaEnv(node)) {
return
}
// Get the env variable name for the error message
const envName = node.property.name || node.property.value || 'unknown'
// Check if it has a fallback (|| or ??)
if (hasFallback(node)) {
return
}
context.report({
node,
messageId: 'noFallback',
data: {
name: `import.meta.env.${envName}`,
},
})
},
}
},
}
/**
* Check if node is import.meta.env.SOMETHING
*/
function isImportMetaEnv(node) {
// Must be a member expression with a property
if (node.type !== 'MemberExpression' || !node.property) {
return false
}
// The object should be import.meta.env
const obj = node.object
if (
obj.type === 'MemberExpression' &&
obj.object.type === 'MetaProperty' &&
obj.object.meta.name === 'import' &&
obj.object.property.name === 'meta' &&
obj.property.name === 'env'
) {
return true
}
return false
}
/**
* Check if the node has a fallback via || or ?? operator
*/
function hasFallback(node) {
let current = node.parent
// Walk up the tree to find if we're in a logical expression with fallback
while (current) {
// Direct fallback: import.meta.env.FOO || 'default' or import.meta.env.FOO ?? 'default'
// Also handles chains: import.meta.env.A || import.meta.env.B || 'default'
if (
current.type === 'LogicalExpression' &&
(current.operator === '||' || current.operator === '??')
) {
// Check if this logical expression chain eventually has a non-env fallback
if (logicalChainHasFallback(current)) {
return true
}
}
// Conditional expression: import.meta.env.FOO ? import.meta.env.FOO : 'default'
if (current.type === 'ConditionalExpression') {
// If used in the test part, it has a fallback (the alternate)
if (node === current.test) {
return true
}
// If used in the consequent and test is an env var check, it's valid
if (node === current.consequent && isImportMetaEnv(current.test)) {
return true
}
}
current = current.parent
}
return false
}
/**
* Check if a logical expression chain has a fallback at the end
* For: A || B || C, we walk up to find the topmost || or ?? and check its right side
*/
function logicalChainHasFallback(node) {
// Walk up to the topmost logical expression in the chain
let topmost = node
while (
topmost.parent &&
topmost.parent.type === 'LogicalExpression' &&
(topmost.parent.operator === '||' || topmost.parent.operator === '??') &&
topmost.parent.left === topmost
) {
topmost = topmost.parent
}
// The rightmost value in the chain is the fallback
// Check that it's not also an import.meta.env (that would mean no real fallback)
const rightmost = getRightmostValue(topmost)
return !isImportMetaEnv(rightmost)
}
/**
* Get the rightmost value in a logical expression chain
*/
function getRightmostValue(node) {
if (
node.type === 'LogicalExpression' &&
(node.operator === '||' || node.operator === '??')
) {
return getRightmostValue(node.right)
}
return node
}