@aurelienbbn/oxlint-plugin-shopify-app

中文文档
0.1.2发布于 2026/10/14MITTypeScriptES Module构建来源已验证近 30 天下载 2431

Custom oxlint rules for Shopify app and extension code.

原始使用文档
文档目录7 节
  1. 27 rules, 4 scopes
  2. Server baseline: every app, no decision needed
  3. App Home recipe: Polaris + native a11y
  4. Category opt-ins: decide first
  5. Other opt-ins
  6. Registered contract inventory
  7. Credited concepts

@aurelienbbn/oxlint-plugin-shopify-app

npm downloads CI license node

27 oxlint rules for Shopify apps and extensions: the static slice of App Store, Built for Shopify, Admin API, and Polaris App Home requirements.

[!WARNING] Passing lint doesn't make an app App Store or Built for Shopify eligible. These rules implement the statically checkable parts only.

pnpm add -D @aurelienbbn/oxlint-plugin-shopify-app oxlint   # oxlint >=1.82.0 <2.0.0

No preset, no autofix: every change means choosing an API, content, layout, or runtime policy.

27 rules, 4 scopes

server baseline   ███████          7   every app, server + route source
App Home recipe   █████████████   13   + 14 jsx-a11y, App Home paths only
category opt-in   ████             4   overrides on the paths the BFS category covers
other opt-in      ███              3   Function source · Polaris manifest · 1 candidate

Rules never infer the app category or extension target. You decide, then scope with overrides.

Server baseline: every app, no decision needed

{
  "jsPlugins": ["@aurelienbbn/oxlint-plugin-shopify-app"],
  "rules": {
    "shopify-app/no-script-tag-api": "error",
    "shopify-app/require-mutation-user-errors": "error",
    "shopify-app/require-idempotent-mutations": "error",
    "shopify-app/no-swallowed-auth-response": "error",
    "shopify-app/no-hardcoded-billing-test-mode": "error",
    "shopify-app/webhook-hmac-verification-shape": "error",
    "shopify-app/no-router-redirect-in-embedded-route": "warn"
  }
}
`mutation($id: ID!, $tags: [String!]!) { tagsAdd(id: $id, tags: $tags) { node { id } } }`; // ❌ no userErrors
`mutation { tagsAdd(id: 1, tags: []) { node { id } userErrors { field message } } }`; // ✅

`mutation($input: RefundInput!) { refundCreate(input: $input) { refund { id } userErrors { message } } }`; // ❌ missingIdempotent
`mutation($input: RefundInput!) { refundCreate(input: $input) @idempotent(key: "4f5b6ebf-143c-4da5-8d0f-fb8553bfd85d") { refund { id } } }`; // ❌ literalIdempotencyKey
`mutation($input: RefundInput!, $key: String!) { refundCreate(input: $input) @idempotent(key: $key) { refund { id } } }`; // ✅

try {
  const { admin } = await authenticate.admin(request);
  return admin;
} catch (error) {
  return data({ error: "Something went wrong" }, { status: 500 });
} // ❌ swallows the Response
try {
  await authenticate.admin(request);
} catch (error) {
  if (error instanceof Response) throw error;
  return null;
} // ✅

await billing.require({ plans: [PRO], isTest: true, onFailure }); // ❌ never bills

Idempotency list: one dated table in the rule source. Published 2025-12-12, reviewed 2026-09-20, required from Admin API 2026-04. Script tags stop running on 2027-03-01: baseline, not a BFS opt-in.

Exact triggers, silent cases, options
Rule❌ Fires on
no-script-tag-apiscriptTagCreate / scriptTagUpdate mutations, REST script_tags requests, generated Script Tag document imports
require-mutation-user-errorsa top-level mutation field in a GraphQL string or template whose selection has no userErrors / *UserErrors field
require-idempotent-mutationsone of the 17 refund, inventory and location mutations in the idempotency changelog without @idempotent (missingIdempotent), or with a string-literal key (literalIdempotencyKey)
no-swallowed-auth-responseauthenticate.admin/webhook/flow/fulfillmentService/pos, authenticate.public.*, billing.require/request/cancel/updateUsageCappedAmount, scopes.request, or a redirect destructured from authenticate.admin, inside a try of the same function whose catch has neither a throw nor an instanceof Response test. These throw a Response as control flow
no-hardcoded-billing-test-modeliteral isTest: true in the first argument of billing.require/request/check/cancel/createUsageRecord, or literal test: true on appSubscriptionCreate / appPurchaseOneTimeCreate
webhook-hmac-verification-shapein files containing the X-Shopify-Hmac-SHA256 header name only: .update(JSON.stringify(...)) on a createHmac chain; createHmac without any timingSafeEqual / safeCompare identifier
no-router-redirect-in-embedded-route ⚠️ warnredirect from react-router or @remix-run/{node,server-runtime,cloudflare} called in a module that also calls authenticate.admin, or anywhere with an https:// or shopify:// destination. Relative redirects usually still work, hence warn
Rule⏭️ Silent onOption (default)
require-mutation-user-errorsunknown fragments, interpolations inside the field, unparsable text. Same-document fragments are followedignoreMutations ([]): payloads without the field. Exclude non-Shopify GraphQL clients via overrides
require-idempotent-mutations—since (unset): the Admin API version you pin, YYYY-01/04/07/10. Below 2026-04 silences missingIdempotent only
no-hardcoded-billing-test-modetest and fixture files—
webhook-hmac-verification-shapeverification delegated to another file; OAuth hmac query checkssafeCompareNames (["timingSafeEqual", "safeCompare"]), replaced not merged

The 17 mutations needing @idempotent: refundCreate · inventoryShipmentReceive · inventoryAdjustQuantities · inventoryMoveQuantities · inventorySetQuantities · inventorySetOnHandQuantities · inventoryShipmentCreateInTransit · inventoryShipmentCreate · inventoryTransferCreate · inventoryTransferCreateAsReadyToShip · inventoryTransferDuplicate · inventoryTransferSetItems · inventorySetScheduledChanges · inventoryActivate · inventoryShipmentAddItems · locationActivate · locationDeactivate

App Home recipe: Polaris + native a11y

Copy the tested App Home recipe; run it on App Home source only:

oxlint --config app-home.oxlintrc.json app
app-home.oxlintrc.json
├── jsx-a11y (14 upstream rules)   native HTML accessibility
└── shopify-app (13 rules)         Polaris App Home v1.0 contracts
      no-nav-emoji · no-viewport-zoom-disable · s-action-slot-contract
      s-button-accessible-name · s-button-submit-no-navigation · s-clickable-accessible-name
      s-form-control-label-required · s-modal-actions-use-slots · s-modal-heading-required
      s-money-field-no-currency-symbol (warn) · s-page-aside-visible
      s-spinner-accessible-label · s-tooltip-no-interactive-content

Keep this recipe on App Home. Checkout and customer-account components have different contracts.

<s-button icon="edit" />                                     // ❌ s-button-accessible-name
<s-button icon="edit" accessibilityLabel="Edit product" />   // ✅

<s-text-field labelAccessibilityVisibility="exclusive" />        // ❌ hiding the label doesn't remove the need for one
<s-search-field label="Search" labelAccessibilityVisibility="exclusive" /> // ✅

<s-money-field label="Price ($)" />                              // ❌ currency formatting is component-owned

Static only: a silent diagnostic is not proof of the rendered state.

Per-rule contracts and what static checks miss
RuleContract
s-action-slot-contractunconditional direct page/modal children, fragments included. primary-action: one s-button with variant=primary. Secondary slots: secondary/auto buttons; pages also accept button groups
s-modal-actions-use-slotsmodal action buttons use the modal action slots. Options components (["s-modal"]), actionElements (["s-button", "button"])
s-modal-heading-requirednonempty heading on modals. Option components (["s-modal"])
s-page-aside-visiblean aside rendered directly under a page whose explicit inlineSize is small or large (it won't render)
s-button-submit-no-navigationstatic href / commandFor that override submit/reset behavior
s-button-accessible-name, s-clickable-accessible-namedirect content, fragments included; icon elements and empty literals don't name an action
s-form-control-label-requiredfields use label; switch and drop zone may use their documented accessibilityLabel instead. Known null, boolean and empty values are rejected
s-spinner-accessible-labelnonempty accessibilityLabel
s-tooltip-no-interactive-contentunconditional intrinsic descendants; stops at user component boundaries
s-money-field-no-currency-symbolUnicode currency symbols in literal labels/placeholders. Doesn't inspect translations or financial arithmetic
no-viewport-zoom-disablemaximum-scale below 5, user-scalable=no / 0
no-nav-emojiemoji in nav labels. Option navComponents (["s-app-nav", "ui-nav-menu"])

Rules read intrinsic elements, static literals, and inline object spreads in effective override order.

✅ Checked⚠️ Still needs review or rendered checks
intrinsic elements, static literals, inline spreadsruntime expressions, later unknown spreads, translated strings, custom component output
direct children and fragmentsconditional rendering, wrappers, button-group contents, other nested elements, user components

Category opt-ins: decide first

RuleTurn it on for
no-draft-order-custom-discountsdiscount-app automation under Built for Shopify 5.5.2. Keep merchant-driven draft-order workflows outside it
no-asset-api-theme-writesapp code under Built for Shopify 3.2.2, after reviewing its page-builder, backup/restore, SEO, content-locking and developer-tooling exceptions
no-admin-rest-apinew public apps under the GraphQL-only policy. Existing public and custom apps: explicit migration decision first
require-fetch-abort-signalexplicit checkout extension dirs where cancellation is request policy. Pair it with runtime deadline evidence: a signal isn't a timeout

[!WARNING] Never scope with a blanket extensions/**: it can't tell checkout, customer-account, admin, Function and theme surfaces apart.

Admin extensions, allowed vs reported, no-admin-rest-api scope

Keep require-fetch-abort-signal off admin UI extensions: a relative fetch("/api/...") to the app's own domain is their documented auto-authenticated path; cancellation is a checkout and customer-account policy. Category applicability and exceptions: Built for Shopify requirements.

Rule✅ Allowed❌ Reported⚠️ Review input
no-draft-order-custom-discountsresponse selections mentioning appliedDiscount (not an input)draft-order mutations applying custom discounts (parsed mutation fields)variable-supplied mutation inputs
no-asset-api-theme-writesREST Asset API GETrecognized write methods; theme file mutations—
require-fetch-abort-signalan explicit usable signal shape; a recognized Request-owned signalfetch (direct or window / globalThis / self) without oneunknown options variables and later overriding spreads aren't proof

no-admin-rest-api:

❌ Reports✅ Allowed⚠️ Needs review
runtime REST resource imports from @shopify/shopify-api/rest/admin, dynamic imports, unshadowed requireGraphQL, Storefront/Ajax APIs, type-only importsSDK instances, wrappers
unshadowed global fetch with a recognizable REST Admin URLunrelated hosts, shadowed functionsdynamically selected API versions and resource names

Other opt-ins

functions-no-unavailable-runtime-apis on JS Function source only. s-known-components on App Home source, beside the recipe. 🧪 no-session-or-token-logging is a candidate, off by default.

<s-stak gap="base" />                           // ❌ unknown element
<s-button varient="primary">Save</s-button>     // ❌ unknown attribute
<s-button variant="primary" onClick={save}>Save</s-button> // ✅ declared attribute and event

s-known-components reads its vocabulary from your project, never from this package: the Custom Elements Manifest named by the customElements field of the nearest installed @shopify/polaris-types (1.0.7 and later). It follows whichever major you install. No package, or no customElements field: the rule stays silent. An explicit manifestPath that can't be read, or a manifest without elements, reports once per file.

Triggers and options
RuleStatus❌ Fires onOptions (default)
functions-no-unavailable-runtime-apisopt-in, JS Function source only; every other surface uses these APIs legitimatelyasync functions, top-level await, .then() on call results, unshadowed Promise, setTimeout, setInterval, fetch, crypto, URL, URLSearchParams, process, Buffer, require, node: imports, Date.now(), zero-arg Date, Math.random(), performance.now()—
s-known-componentsopt-in, App Home sources-* elements the manifest doesn't declare; attributes that aren't a declared attribute, public field or on<event> handler. Always allowed: key, ref, slot, children, id, data-*. Children of allowed elements (App Bridge nav links) keep their own contractmanifestPath (discovered), allowElements (["s-app-nav", "s-app-window"], replaced), allowAttributes ([])
no-session-or-token-logging🧪 candidate, off by defaultconsole / logger / log calls exposing session, accessToken, access_token, sessionToken, idToken (identifier, shorthand, terminal member, spread, template hole, JSON.stringify), or a payload destructured from authenticate.webhookloggerObjects (["console", "logger", "log"]), sensitiveNames (the 5 above), webhookPayloadNames (["payload"]), each replaced not merged

no-session-or-token-logging is domain-neutral apart from the webhook payload hook: prefer a core secrets-in-logs rule once one exists.

Out of scope, and who owns it
ConcernOwner
native HTML accessibilityupstream jsx-a11y rules
Polaris property unionsShopify's component types
contrast, attention, copy quality, keyboard behavior, responsive layout, runtime API behaviorsemantic review or browser/runtime evidence
Page breadcrumbs, Select option content❌ no blanket rule: Shopify's guidance conflicts
Script Tag scopeimports scoped to generated/admin sources; incidental URL strings aren't API calls

Every rule independently implements Shopify documentation concepts; source carries greppable @attribution tags; no implementation or prose vendored.

Sources, by review date
ReviewedSources
2026-09-05 · PolarisREST Admin migration policy, Button, Clickable, Spinner, form components, Page, Modal, Tooltip, Money field
2026-09-20 · server & APIAdmin GraphQL reference, idempotency changelog, idempotency guide, control flow of shopify-app-js, React Router app template, billing API reference, webhook HTTPS delivery, JavaScript for Functions, Function input/output limits, protected customer data, Script Tag deprecation changelog
2026-09-24 · Polaris manifestApp Home web components, the customElements field and manifest schema of the Custom Elements Manifest format, as shipped by @shopify/polaris-types 1.1.0 and 2.0.0-rc.0 (same tags and attributes)

Registered contract inventory

Generated from package exports by pnpm catalog. Rule-specific options and limitations are described above and in the source tests.

Rule/checkTrigger or review scope
functions-no-unavailable-runtime-apisDisallow async code, timers, fetch, crypto, URL, process, Buffer, require, node: imports, and clock or random reads in Shopify Function source; scope it to Function directories with overrides.
no-admin-rest-apiDisallow Shopify REST Admin resource imports and unshadowed fetch calls to recognizable REST Admin endpoints; enable for new public apps.
no-asset-api-theme-writesDisallow theme file writes through the Asset API or theme file mutations.
no-draft-order-custom-discountsDisallow draft order mutations that apply custom discounts.
no-hardcoded-billing-test-modeDisallow literal isTest: true in billing.* calls and literal test: true on appSubscriptionCreate/appPurchaseOneTimeCreate outside test files, because test charges never bill the merchant.
no-nav-emojiDisallow emoji inside app navigation labels. Option navComponents overrides the checked element names.
no-router-redirect-in-embedded-routePrefer the redirect helper returned by authenticate.admin over the router's redirect in modules that authenticate admin requests, and always for https:// or shopify:// destinations.
no-script-tag-apiDisallow the Script Tag API, which stops running on 2027-03-01, in favor of Web Pixel and theme app extensions.
no-session-or-token-loggingDisallow passing session objects, access or session tokens, or an authenticate.webhook payload to console/logger calls. Options loggerObjects, sensitiveNames and webhookPayloadNames override the matched names.
no-swallowed-auth-responseDisallow authenticate.*, billing.require/request/cancel/updateUsageCappedAmount, scopes.request and the embedded redirect helper inside a try whose catch never rethrows, because they throw a Response as control flow.
no-viewport-zoom-disableDisallow viewport meta tags that block pinch zoom.
require-fetch-abort-signalRequire fetch calls (direct or window/globalThis/self qualified) to pass an AbortSignal.
require-idempotent-mutationsRequire @idempotent(key: $variable) on the 17 inventory, refund and location mutations that reject calls without it from Admin API 2026-04, and disallow literal keys. Option since names the API version the project pins.
require-mutation-user-errorsRequire every GraphQL mutation field to select userErrors (or a *UserErrors field). Option ignoreMutations lists mutation names without such a payload field.
s-action-slot-contractValidate statically assigned Polaris page/modal action slot element types, variants, and single-primary cardinality.
s-button-accessible-nameRequire an accessibilityLabel on statically unnamed Polaris buttons, including icon-only buttons.
s-button-submit-no-navigationDisallow static href or commandFor values that override a Polaris button's submit or reset behavior.
s-clickable-accessible-nameRequire accessibilityLabel on Polaris clickable components without statically readable content.
s-form-control-label-requiredRequire labels on Polaris form fields; switch and drop zone may use their documented accessibilityLabel alternative.
s-known-componentsDisallow s-* elements and attributes that the project's installed @shopify/polaris-types Custom Elements Manifest does not declare; silent when no manifest resolves. Options manifestPath, allowElements and allowAttributes.
s-modal-actions-use-slotsRequire modal action buttons to use the modal action slots. Options components and actionElements override the checked element names.
s-modal-heading-requiredRequire a heading attribute on modal elements. Option components overrides the checked element names.
s-money-field-no-currency-symbolDisallow literal currency symbols in Polaris money-field labels and placeholders, whose currency formatting is component-owned.
s-page-aside-visibleDisallow s-page aside content when an explicit small or large inlineSize prevents it from rendering.
s-spinner-accessible-labelRequire a nonempty accessibilityLabel on Polaris spinners.
s-tooltip-no-interactive-contentDisallow statically interactive intrinsic descendants inside Polaris tooltips; custom component and conditional output require review.
webhook-hmac-verification-shapeIn files reading the X-Shopify-Hmac-SHA256 header, disallow hashing JSON.stringify output and require a timing-safe comparison. Option safeCompareNames overrides the accepted comparison identifiers.

Credited concepts

版本与发布记录

@aurelienbbn/oxlint-plugin-shopify-app 共发布 3 个版本,首次发布于 2026/09/25;过去 12 个月发布了 3 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.2latest2026/10/1424 天前
0.1.12026/09/251 个月前
0.1.02026/09/251 个月前

关于 @aurelienbbn/oxlint-plugin-shopify-app 的常见问题

@aurelienbbn/oxlint-plugin-shopify-app 是什么?

Custom oxlint rules for Shopify app and extension code.

如何安装 @aurelienbbn/oxlint-plugin-shopify-app?

在项目目录运行 npm install @aurelienbbn/oxlint-plugin-shopify-app,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:oxlint(>=1.82.0 <2.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@aurelienbbn/oxlint-plugin-shopify-app 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@aurelienbbn/oxlint-plugin-shopify-app 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@aurelienbbn/oxlint-plugin-shopify-app 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js ^22.19.0 || ^24.11.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@aurelienbbn/oxlint-plugin-shopify-app 可以商用吗?

@aurelienbbn/oxlint-plugin-shopify-app 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@aurelienbbn/oxlint-plugin-shopify-app 的下载量有多少?

根据 npm 官方下载统计,@aurelienbbn/oxlint-plugin-shopify-app 在 2026/10/07 – 2026/11/05 这 30 天内被下载 2,431 次,日均约 81 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@aurelienbbn/oxlint-plugin-shopify-app 还在维护吗?

最新版本 0.1.2 发布于 2026/10/14(24 天前);至今共发布 3 个版本,其中过去 12 个月发布了 3 个;首次发布于 2026/09/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。