better-ship 0.35.0 → 0.36.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.
@@ -0,0 +1,10 @@
1
+ //#region src/infrastructure/tanstack/oxlint/index.d.ts
2
+ /**
3
+ * Oxlint plugin for TanStack Form and Query.
4
+ * Enable the `tanstack/*` rules from `jsPlugins`.
5
+ * Ported from Suparelay `packages/tanstack-lint`. The rule ids are unchanged.
6
+ */
7
+ declare const tanstackPlugin: import("@oxlint/plugins").Plugin;
8
+ //#endregion
9
+ export { tanstackPlugin as default };
10
+ //# sourceMappingURL=oxlint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oxlint.d.ts","names":[],"sources":["../../src/infrastructure/tanstack/oxlint/index.ts"],"mappings":";;;;;;cAaM,0CAAc"}
@@ -0,0 +1,256 @@
1
+ import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
2
+
3
+ //#region src/infrastructure/tanstack/oxlint/form-options.ts
4
+ const FORM_HOOKS = /* @__PURE__ */ new Set(["useForm", "useAppForm"]);
5
+ const SAFETY = /(?:^|[^\p{L}\p{N}_])SAFETY\s*:\s*\S/u;
6
+ /** The options object of a `useForm({ … })` or `useAppForm({ … })` call. */
7
+ function formOptions(node) {
8
+ if (node.callee.type !== "Identifier" || !FORM_HOOKS.has(node.callee.name)) return null;
9
+ const [options] = node.arguments;
10
+ return options?.type === "ObjectExpression" ? options : null;
11
+ }
12
+ /** A plain `name: value` property of an object literal. */
13
+ function propertyNamed(object, name) {
14
+ for (const property of object.properties) {
15
+ if (property.type !== "Property" || property.computed) continue;
16
+ const key = property.key;
17
+ if (key.type === "Identifier" && key.name === name) return property;
18
+ if (key.type === "Literal" && key.value === name) return property;
19
+ }
20
+ return null;
21
+ }
22
+ /** A `// SAFETY:` comment directly before the node allows one exception. */
23
+ function hasSafetyComment(sourceCode, node) {
24
+ return sourceCode.getCommentsBefore(node).some((comment) => SAFETY.test(comment.value));
25
+ }
26
+ /** Every node under `root`. Parent links are not followed. */
27
+ function* descendants(root) {
28
+ for (const [key, value] of Object.entries(root)) {
29
+ if (key === "parent") continue;
30
+ const children = Array.isArray(value) ? value : [value];
31
+ for (const child of children) if (isAstNode(child)) {
32
+ yield child;
33
+ yield* descendants(child);
34
+ }
35
+ }
36
+ }
37
+ function isAstNode(value) {
38
+ return value instanceof Object && "type" in value && "start" in value;
39
+ }
40
+
41
+ //#endregion
42
+ //#region src/infrastructure/tanstack/oxlint/form-schema-validators.ts
43
+ const DYNAMIC_SLOTS = /* @__PURE__ */ new Set(["onDynamic", "onDynamicAsync"]);
44
+ /**
45
+ * Require one named schema on `useForm` and `useAppForm`.
46
+ * `validationLogic` must call `revalidateLogic`. The schema sits on `validators.onDynamic`.
47
+ * A `// SAFETY:` comment before the call allows one form.
48
+ */
49
+ const formSchemaValidatorsRule = defineRule({
50
+ meta: {
51
+ type: "problem",
52
+ docs: { description: "Require `validationLogic: revalidateLogic(…)` and `validators: { onDynamic: Schema }` on every TanStack form." },
53
+ messages: {
54
+ logic: "Choose the timing with validationLogic: revalidateLogic({ mode, modeAfterSubmission }).",
55
+ missing: "Validate the form with one schema: validators: { onDynamic: Schema }.",
56
+ slot: "Replace {{slot}} with onDynamic; revalidateLogic sets the timing.",
57
+ inline: "Pass a schema, not a function: validators: { onDynamic: Schema }."
58
+ }
59
+ },
60
+ createOnce(context) {
61
+ return { CallExpression(node) {
62
+ const options = formOptions(node);
63
+ if (options === null || hasSafetyComment(context.sourceCode, node)) return;
64
+ const logic = propertyNamed(options, "validationLogic");
65
+ if (logic === null || logic.value.type !== "CallExpression" || logic.value.callee.type !== "Identifier" || logic.value.callee.name !== "revalidateLogic") context.report({
66
+ node: logic ?? node,
67
+ messageId: "logic"
68
+ });
69
+ const validators = propertyNamed(options, "validators");
70
+ if (validators === null || validators.value.type !== "ObjectExpression") {
71
+ context.report({
72
+ node: validators ?? node,
73
+ messageId: "missing"
74
+ });
75
+ return;
76
+ }
77
+ for (const slot of validators.value.properties) {
78
+ if (slot.type !== "Property" || slot.key.type !== "Identifier") continue;
79
+ if (!DYNAMIC_SLOTS.has(slot.key.name)) context.report({
80
+ node: slot,
81
+ messageId: "slot",
82
+ data: { slot: slot.key.name }
83
+ });
84
+ else if (slot.value.type !== "Identifier" && slot.value.type !== "MemberExpression") context.report({
85
+ node: slot.value,
86
+ messageId: "inline"
87
+ });
88
+ }
89
+ } };
90
+ }
91
+ });
92
+
93
+ //#endregion
94
+ //#region src/infrastructure/tanstack/oxlint/form-stable-default-values.ts
95
+ /** A call whose result changes between renders, such as the current time or a random id. */
96
+ function isUnstable(node) {
97
+ if (node.type === "NewExpression") return node.callee.type === "Identifier" && node.callee.name === "Date";
98
+ if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return false;
99
+ const { object, property } = node.callee;
100
+ if (object.type !== "Identifier" || property.type !== "Identifier") return false;
101
+ return object.name === "Date" && property.name === "now" || object.name === "Math" && property.name === "random" || object.name === "crypto" && property.name === "randomUUID";
102
+ }
103
+ /**
104
+ * Reject a time or random value written inside `defaultValues`.
105
+ * TanStack Form applies a changed default to an untouched form on the next render.
106
+ */
107
+ const formStableDefaultValuesRule = defineRule({
108
+ meta: {
109
+ type: "problem",
110
+ docs: { description: "Disallow time or random values computed inline in `defaultValues`." },
111
+ messages: { unstable: "Compute this default once with useState(() => …) or a module constant; an inline value resets the form on render." }
112
+ },
113
+ createOnce(context) {
114
+ return { CallExpression(node) {
115
+ const options = formOptions(node);
116
+ const defaults = options === null ? null : propertyNamed(options, "defaultValues");
117
+ if (defaults === null) return;
118
+ for (const child of [defaults.value, ...descendants(defaults.value)]) if (isUnstable(child)) context.report({
119
+ node: child,
120
+ messageId: "unstable"
121
+ });
122
+ } };
123
+ }
124
+ });
125
+
126
+ //#endregion
127
+ //#region src/infrastructure/tanstack/oxlint/no-field-validators.ts
128
+ const FIELD_COMPONENTS = /* @__PURE__ */ new Set(["Field", "AppField"]);
129
+ /** Reject field-level `validators`. The form schema is the only validation source. */
130
+ const noFieldValidatorsRule = defineRule({
131
+ meta: {
132
+ type: "problem",
133
+ docs: { description: "Disallow field-level `validators`; the form schema validates." },
134
+ messages: { fieldValidators: "Remove validators from the field. Put the rule in the form schema." }
135
+ },
136
+ createOnce(context) {
137
+ return {
138
+ JSXOpeningElement(node) {
139
+ if (node.name.type !== "JSXMemberExpression") return;
140
+ if (!FIELD_COMPONENTS.has(node.name.property.name)) return;
141
+ for (const attribute of node.attributes) if (attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "validators") context.report({
142
+ node: attribute,
143
+ messageId: "fieldValidators"
144
+ });
145
+ },
146
+ CallExpression(node) {
147
+ if (node.callee.type !== "Identifier" || node.callee.name !== "useField") return;
148
+ const [options] = node.arguments;
149
+ if (options?.type !== "ObjectExpression") return;
150
+ const validators = propertyNamed(options, "validators");
151
+ if (validators !== null) context.report({
152
+ node: validators,
153
+ messageId: "fieldValidators"
154
+ });
155
+ }
156
+ };
157
+ }
158
+ });
159
+
160
+ //#endregion
161
+ //#region src/infrastructure/tanstack/oxlint/no-submit-branching.ts
162
+ const BRANCH_OPERATORS = /* @__PURE__ */ new Set([
163
+ "??",
164
+ "||",
165
+ "&&"
166
+ ]);
167
+ function isBranch(node) {
168
+ if (node.type === "IfStatement" || node.type === "ConditionalExpression") return true;
169
+ return node.type === "LogicalExpression" && BRANCH_OPERATORS.has(node.operator);
170
+ }
171
+ /**
172
+ * Reject a condition inside `onSubmit`.
173
+ * The schema already accepted the values. A `// SAFETY:` comment on `onSubmit` allows one check.
174
+ */
175
+ const noSubmitBranchingRule = defineRule({
176
+ meta: {
177
+ type: "problem",
178
+ docs: { description: "Disallow conditions in a TanStack form's `onSubmit`." },
179
+ messages: { branch: "Move this check into the form schema. Use Schema.parse(value) when the submitted type must be narrowed." }
180
+ },
181
+ createOnce(context) {
182
+ return { CallExpression(node) {
183
+ const options = formOptions(node);
184
+ const submit = options === null ? null : propertyNamed(options, "onSubmit");
185
+ if (submit === null || hasSafetyComment(context.sourceCode, submit)) return;
186
+ for (const child of descendants(submit.value)) if (isBranch(child)) context.report({
187
+ node: child,
188
+ messageId: "branch"
189
+ });
190
+ } };
191
+ }
192
+ });
193
+
194
+ //#endregion
195
+ //#region src/infrastructure/tanstack/oxlint/query-key-from-factory.ts
196
+ const CACHE_METHODS = /* @__PURE__ */ new Set([
197
+ "getQueryData",
198
+ "setQueryData",
199
+ "getQueryState",
200
+ "ensureQueryData"
201
+ ]);
202
+ /**
203
+ * Reject a query key array written outside a `*-queries.ts` factory.
204
+ * Invalidation then reads the same key the factory stored.
205
+ */
206
+ const queryKeyFromFactoryRule = defineRule({
207
+ meta: {
208
+ type: "problem",
209
+ docs: { description: "Disallow query key arrays outside `*-queries.ts` factory files." },
210
+ messages: { literal: "Read this key from the entity's query factory, for example entityQueries.lists() or entityQueries.detail(input).queryKey." }
211
+ },
212
+ createOnce(context) {
213
+ const inFactory = () => context.filename.endsWith("-queries.ts");
214
+ return {
215
+ Property(node) {
216
+ if (inFactory() || node.computed || node.key.type !== "Identifier") return;
217
+ if (node.key.name === "queryKey" && node.value.type === "ArrayExpression") context.report({
218
+ node: node.value,
219
+ messageId: "literal"
220
+ });
221
+ },
222
+ CallExpression(node) {
223
+ if (inFactory() || node.callee.type !== "MemberExpression") return;
224
+ const method = node.callee.property;
225
+ if (method.type !== "Identifier" || !CACHE_METHODS.has(method.name)) return;
226
+ const [key] = node.arguments;
227
+ if (key?.type === "ArrayExpression") context.report({
228
+ node: key,
229
+ messageId: "literal"
230
+ });
231
+ }
232
+ };
233
+ }
234
+ });
235
+
236
+ //#endregion
237
+ //#region src/infrastructure/tanstack/oxlint/index.ts
238
+ /**
239
+ * Oxlint plugin for TanStack Form and Query.
240
+ * Enable the `tanstack/*` rules from `jsPlugins`.
241
+ * Ported from Suparelay `packages/tanstack-lint`. The rule ids are unchanged.
242
+ */
243
+ const tanstackPlugin = eslintCompatPlugin({
244
+ meta: { name: "tanstack" },
245
+ rules: {
246
+ "form-schema-validators": formSchemaValidatorsRule,
247
+ "form-stable-default-values": formStableDefaultValuesRule,
248
+ "no-field-validators": noFieldValidatorsRule,
249
+ "no-submit-branching": noSubmitBranchingRule,
250
+ "query-key-from-factory": queryKeyFromFactoryRule
251
+ }
252
+ });
253
+
254
+ //#endregion
255
+ export { tanstackPlugin as default };
256
+ //# sourceMappingURL=oxlint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oxlint.js","names":[],"sources":["../../src/infrastructure/tanstack/oxlint/form-options.ts","../../src/infrastructure/tanstack/oxlint/form-schema-validators.ts","../../src/infrastructure/tanstack/oxlint/form-stable-default-values.ts","../../src/infrastructure/tanstack/oxlint/no-field-validators.ts","../../src/infrastructure/tanstack/oxlint/no-submit-branching.ts","../../src/infrastructure/tanstack/oxlint/query-key-from-factory.ts","../../src/infrastructure/tanstack/oxlint/index.ts"],"sourcesContent":["import type { ESTree, SourceCode } from '@oxlint/plugins'\n\nconst FORM_HOOKS = new Set(['useForm', 'useAppForm'])\nconst SAFETY = /(?:^|[^\\p{L}\\p{N}_])SAFETY\\s*:\\s*\\S/u\n\n/** The options object of a `useForm({ … })` or `useAppForm({ … })` call. */\nexport function formOptions(node: ESTree.CallExpression): ESTree.ObjectExpression | null {\n if (node.callee.type !== 'Identifier' || !FORM_HOOKS.has(node.callee.name)) return null\n const [options] = node.arguments\n return options?.type === 'ObjectExpression' ? options : null\n}\n\n/** A plain `name: value` property of an object literal. */\nexport function propertyNamed(\n object: ESTree.ObjectExpression,\n name: string,\n): ESTree.ObjectProperty | null {\n for (const property of object.properties) {\n if (property.type !== 'Property' || property.computed) continue\n const key = property.key\n if (key.type === 'Identifier' && key.name === name) return property\n if (key.type === 'Literal' && key.value === name) return property\n }\n return null\n}\n\n/** A `// SAFETY:` comment directly before the node allows one exception. */\nexport function hasSafetyComment(sourceCode: SourceCode, node: ESTree.Node): boolean {\n return sourceCode.getCommentsBefore(node).some((comment) => SAFETY.test(comment.value))\n}\n\n/** Every node under `root`. Parent links are not followed. */\nexport function* descendants(root: ESTree.Node): Generator<ESTree.Node> {\n for (const [key, value] of Object.entries(root)) {\n if (key === 'parent') continue\n const children = Array.isArray(value) ? value : [value]\n for (const child of children) {\n if (isAstNode(child)) {\n yield child\n yield* descendants(child)\n }\n }\n }\n}\n\ntype AstChild =\n | { type?: string; start?: number }\n | string\n | number\n | boolean\n | bigint\n | symbol\n | null\n | undefined\n\nfunction isAstNode(value: AstChild): value is ESTree.Node {\n return value instanceof Object && 'type' in value && 'start' in value\n}\n","import { defineRule } from '@oxlint/plugins'\n\nimport { formOptions, hasSafetyComment, propertyNamed } from './form-options.ts'\n\nconst DYNAMIC_SLOTS = new Set(['onDynamic', 'onDynamicAsync'])\n\n/**\n * Require one named schema on `useForm` and `useAppForm`.\n * `validationLogic` must call `revalidateLogic`. The schema sits on `validators.onDynamic`.\n * A `// SAFETY:` comment before the call allows one form.\n */\nexport const formSchemaValidatorsRule = defineRule({\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require `validationLogic: revalidateLogic(…)` and `validators: { onDynamic: Schema }` on every TanStack form.',\n },\n messages: {\n logic:\n 'Choose the timing with validationLogic: revalidateLogic({ mode, modeAfterSubmission }).',\n missing: 'Validate the form with one schema: validators: { onDynamic: Schema }.',\n slot: 'Replace {{slot}} with onDynamic; revalidateLogic sets the timing.',\n inline: 'Pass a schema, not a function: validators: { onDynamic: Schema }.',\n },\n },\n createOnce(context) {\n return {\n CallExpression(node) {\n const options = formOptions(node)\n if (options === null || hasSafetyComment(context.sourceCode, node)) return\n const logic = propertyNamed(options, 'validationLogic')\n if (\n logic === null ||\n logic.value.type !== 'CallExpression' ||\n logic.value.callee.type !== 'Identifier' ||\n logic.value.callee.name !== 'revalidateLogic'\n ) {\n context.report({ node: logic ?? node, messageId: 'logic' })\n }\n const validators = propertyNamed(options, 'validators')\n if (validators === null || validators.value.type !== 'ObjectExpression') {\n context.report({ node: validators ?? node, messageId: 'missing' })\n return\n }\n for (const slot of validators.value.properties) {\n if (slot.type !== 'Property' || slot.key.type !== 'Identifier') continue\n if (!DYNAMIC_SLOTS.has(slot.key.name)) {\n context.report({ node: slot, messageId: 'slot', data: { slot: slot.key.name } })\n } else if (slot.value.type !== 'Identifier' && slot.value.type !== 'MemberExpression') {\n context.report({ node: slot.value, messageId: 'inline' })\n }\n }\n },\n }\n },\n})\n","import { defineRule } from '@oxlint/plugins'\nimport type { ESTree } from '@oxlint/plugins'\n\nimport { descendants, formOptions, propertyNamed } from './form-options.ts'\n\n/** A call whose result changes between renders, such as the current time or a random id. */\nfunction isUnstable(node: ESTree.Node): boolean {\n if (node.type === 'NewExpression') {\n return node.callee.type === 'Identifier' && node.callee.name === 'Date'\n }\n if (node.type !== 'CallExpression' || node.callee.type !== 'MemberExpression') return false\n const { object, property } = node.callee\n if (object.type !== 'Identifier' || property.type !== 'Identifier') return false\n return (\n (object.name === 'Date' && property.name === 'now') ||\n (object.name === 'Math' && property.name === 'random') ||\n (object.name === 'crypto' && property.name === 'randomUUID')\n )\n}\n\n/**\n * Reject a time or random value written inside `defaultValues`.\n * TanStack Form applies a changed default to an untouched form on the next render.\n */\nexport const formStableDefaultValuesRule = defineRule({\n meta: {\n type: 'problem',\n docs: { description: 'Disallow time or random values computed inline in `defaultValues`.' },\n messages: {\n unstable:\n 'Compute this default once with useState(() => …) or a module constant; an inline value resets the form on render.',\n },\n },\n createOnce(context) {\n return {\n CallExpression(node) {\n const options = formOptions(node)\n const defaults = options === null ? null : propertyNamed(options, 'defaultValues')\n if (defaults === null) return\n for (const child of [defaults.value, ...descendants(defaults.value)]) {\n if (isUnstable(child)) context.report({ node: child, messageId: 'unstable' })\n }\n },\n }\n },\n})\n","import { defineRule } from '@oxlint/plugins'\n\nimport { propertyNamed } from './form-options.ts'\n\nconst FIELD_COMPONENTS = new Set(['Field', 'AppField'])\n\n/** Reject field-level `validators`. The form schema is the only validation source. */\nexport const noFieldValidatorsRule = defineRule({\n meta: {\n type: 'problem',\n docs: { description: 'Disallow field-level `validators`; the form schema validates.' },\n messages: {\n fieldValidators: 'Remove validators from the field. Put the rule in the form schema.',\n },\n },\n createOnce(context) {\n return {\n JSXOpeningElement(node) {\n if (node.name.type !== 'JSXMemberExpression') return\n if (!FIELD_COMPONENTS.has(node.name.property.name)) return\n for (const attribute of node.attributes) {\n if (\n attribute.type === 'JSXAttribute' &&\n attribute.name.type === 'JSXIdentifier' &&\n attribute.name.name === 'validators'\n ) {\n context.report({ node: attribute, messageId: 'fieldValidators' })\n }\n }\n },\n CallExpression(node) {\n if (node.callee.type !== 'Identifier' || node.callee.name !== 'useField') return\n const [options] = node.arguments\n if (options?.type !== 'ObjectExpression') return\n const validators = propertyNamed(options, 'validators')\n if (validators !== null) context.report({ node: validators, messageId: 'fieldValidators' })\n },\n }\n },\n})\n","import { defineRule } from '@oxlint/plugins'\nimport type { ESTree } from '@oxlint/plugins'\n\nimport { descendants, formOptions, hasSafetyComment, propertyNamed } from './form-options.ts'\n\nconst BRANCH_OPERATORS = new Set(['??', '||', '&&'])\n\nfunction isBranch(node: ESTree.Node): boolean {\n if (node.type === 'IfStatement' || node.type === 'ConditionalExpression') return true\n return node.type === 'LogicalExpression' && BRANCH_OPERATORS.has(node.operator)\n}\n\n/**\n * Reject a condition inside `onSubmit`.\n * The schema already accepted the values. A `// SAFETY:` comment on `onSubmit` allows one check.\n */\nexport const noSubmitBranchingRule = defineRule({\n meta: {\n type: 'problem',\n docs: { description: \"Disallow conditions in a TanStack form's `onSubmit`.\" },\n messages: {\n branch:\n 'Move this check into the form schema. Use Schema.parse(value) when the submitted type must be narrowed.',\n },\n },\n createOnce(context) {\n return {\n CallExpression(node) {\n const options = formOptions(node)\n const submit = options === null ? null : propertyNamed(options, 'onSubmit')\n if (submit === null || hasSafetyComment(context.sourceCode, submit)) return\n for (const child of descendants(submit.value)) {\n if (isBranch(child)) context.report({ node: child, messageId: 'branch' })\n }\n },\n }\n },\n})\n","import { defineRule } from '@oxlint/plugins'\n\nconst CACHE_METHODS = new Set(['getQueryData', 'setQueryData', 'getQueryState', 'ensureQueryData'])\n\n/**\n * Reject a query key array written outside a `*-queries.ts` factory.\n * Invalidation then reads the same key the factory stored.\n */\nexport const queryKeyFromFactoryRule = defineRule({\n meta: {\n type: 'problem',\n docs: { description: 'Disallow query key arrays outside `*-queries.ts` factory files.' },\n messages: {\n literal:\n \"Read this key from the entity's query factory, for example entityQueries.lists() or entityQueries.detail(input).queryKey.\",\n },\n },\n createOnce(context) {\n const inFactory = () => context.filename.endsWith('-queries.ts')\n return {\n Property(node) {\n if (inFactory() || node.computed || node.key.type !== 'Identifier') return\n if (node.key.name === 'queryKey' && node.value.type === 'ArrayExpression') {\n context.report({ node: node.value, messageId: 'literal' })\n }\n },\n CallExpression(node) {\n if (inFactory() || node.callee.type !== 'MemberExpression') return\n const method = node.callee.property\n if (method.type !== 'Identifier' || !CACHE_METHODS.has(method.name)) return\n const [key] = node.arguments\n if (key?.type === 'ArrayExpression') context.report({ node: key, messageId: 'literal' })\n },\n }\n },\n})\n","import { eslintCompatPlugin } from '@oxlint/plugins'\n\nimport { formSchemaValidatorsRule } from './form-schema-validators.ts'\nimport { formStableDefaultValuesRule } from './form-stable-default-values.ts'\nimport { noFieldValidatorsRule } from './no-field-validators.ts'\nimport { noSubmitBranchingRule } from './no-submit-branching.ts'\nimport { queryKeyFromFactoryRule } from './query-key-from-factory.ts'\n\n/**\n * Oxlint plugin for TanStack Form and Query.\n * Enable the `tanstack/*` rules from `jsPlugins`.\n * Ported from Suparelay `packages/tanstack-lint`. The rule ids are unchanged.\n */\nconst tanstackPlugin = eslintCompatPlugin({\n meta: { name: 'tanstack' },\n rules: {\n 'form-schema-validators': formSchemaValidatorsRule,\n 'form-stable-default-values': formStableDefaultValuesRule,\n 'no-field-validators': noFieldValidatorsRule,\n 'no-submit-branching': noSubmitBranchingRule,\n 'query-key-from-factory': queryKeyFromFactoryRule,\n },\n})\n\nexport default tanstackPlugin\n"],"mappings":";;;AAEA,MAAM,6BAAa,IAAI,IAAI,CAAC,WAAW,YAAY,CAAC;AACpD,MAAM,SAAS;;AAGf,SAAgB,YAAY,MAA6D;CACvF,IAAI,KAAK,OAAO,SAAS,gBAAgB,CAAC,WAAW,IAAI,KAAK,OAAO,IAAI,GAAG,OAAO;CACnF,MAAM,CAAC,WAAW,KAAK;CACvB,OAAO,SAAS,SAAS,qBAAqB,UAAU;AAC1D;;AAGA,SAAgB,cACd,QACA,MAC8B;CAC9B,KAAK,MAAM,YAAY,OAAO,YAAY;EACxC,IAAI,SAAS,SAAS,cAAc,SAAS,UAAU;EACvD,MAAM,MAAM,SAAS;EACrB,IAAI,IAAI,SAAS,gBAAgB,IAAI,SAAS,MAAM,OAAO;EAC3D,IAAI,IAAI,SAAS,aAAa,IAAI,UAAU,MAAM,OAAO;CAC3D;CACA,OAAO;AACT;;AAGA,SAAgB,iBAAiB,YAAwB,MAA4B;CACnF,OAAO,WAAW,kBAAkB,IAAI,CAAC,CAAC,MAAM,YAAY,OAAO,KAAK,QAAQ,KAAK,CAAC;AACxF;;AAGA,UAAiB,YAAY,MAA2C;CACtE,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAAG;EAC/C,IAAI,QAAQ,UAAU;EACtB,MAAM,WAAW,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;EACtD,KAAK,MAAM,SAAS,UAClB,IAAI,UAAU,KAAK,GAAG;GACpB,MAAM;GACN,OAAO,YAAY,KAAK;EAC1B;CAEJ;AACF;AAYA,SAAS,UAAU,OAAuC;CACxD,OAAO,iBAAiB,UAAU,UAAU,SAAS,WAAW;AAClE;;;;ACrDA,MAAM,gCAAgB,IAAI,IAAI,CAAC,aAAa,gBAAgB,CAAC;;;;;;AAO7D,MAAa,2BAA2B,WAAW;CACjD,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,gHACJ;EACA,UAAU;GACR,OACE;GACF,SAAS;GACT,MAAM;GACN,QAAQ;EACV;CACF;CACA,WAAW,SAAS;EAClB,OAAO,EACL,eAAe,MAAM;GACnB,MAAM,UAAU,YAAY,IAAI;GAChC,IAAI,YAAY,QAAQ,iBAAiB,QAAQ,YAAY,IAAI,GAAG;GACpE,MAAM,QAAQ,cAAc,SAAS,iBAAiB;GACtD,IACE,UAAU,QACV,MAAM,MAAM,SAAS,oBACrB,MAAM,MAAM,OAAO,SAAS,gBAC5B,MAAM,MAAM,OAAO,SAAS,mBAE5B,QAAQ,OAAO;IAAE,MAAM,SAAS;IAAM,WAAW;GAAQ,CAAC;GAE5D,MAAM,aAAa,cAAc,SAAS,YAAY;GACtD,IAAI,eAAe,QAAQ,WAAW,MAAM,SAAS,oBAAoB;IACvE,QAAQ,OAAO;KAAE,MAAM,cAAc;KAAM,WAAW;IAAU,CAAC;IACjE;GACF;GACA,KAAK,MAAM,QAAQ,WAAW,MAAM,YAAY;IAC9C,IAAI,KAAK,SAAS,cAAc,KAAK,IAAI,SAAS,cAAc;IAChE,IAAI,CAAC,cAAc,IAAI,KAAK,IAAI,IAAI,GAClC,QAAQ,OAAO;KAAE,MAAM;KAAM,WAAW;KAAQ,MAAM,EAAE,MAAM,KAAK,IAAI,KAAK;IAAE,CAAC;SAC1E,IAAI,KAAK,MAAM,SAAS,gBAAgB,KAAK,MAAM,SAAS,oBACjE,QAAQ,OAAO;KAAE,MAAM,KAAK;KAAO,WAAW;IAAS,CAAC;GAE5D;EACF,EACF;CACF;AACF,CAAC;;;;;AClDD,SAAS,WAAW,MAA4B;CAC9C,IAAI,KAAK,SAAS,iBAChB,OAAO,KAAK,OAAO,SAAS,gBAAgB,KAAK,OAAO,SAAS;CAEnE,IAAI,KAAK,SAAS,oBAAoB,KAAK,OAAO,SAAS,oBAAoB,OAAO;CACtF,MAAM,EAAE,QAAQ,aAAa,KAAK;CAClC,IAAI,OAAO,SAAS,gBAAgB,SAAS,SAAS,cAAc,OAAO;CAC3E,OACG,OAAO,SAAS,UAAU,SAAS,SAAS,SAC5C,OAAO,SAAS,UAAU,SAAS,SAAS,YAC5C,OAAO,SAAS,YAAY,SAAS,SAAS;AAEnD;;;;;AAMA,MAAa,8BAA8B,WAAW;CACpD,MAAM;EACJ,MAAM;EACN,MAAM,EAAE,aAAa,qEAAqE;EAC1F,UAAU,EACR,UACE,oHACJ;CACF;CACA,WAAW,SAAS;EAClB,OAAO,EACL,eAAe,MAAM;GACnB,MAAM,UAAU,YAAY,IAAI;GAChC,MAAM,WAAW,YAAY,OAAO,OAAO,cAAc,SAAS,eAAe;GACjF,IAAI,aAAa,MAAM;GACvB,KAAK,MAAM,SAAS,CAAC,SAAS,OAAO,GAAG,YAAY,SAAS,KAAK,CAAC,GACjE,IAAI,WAAW,KAAK,GAAG,QAAQ,OAAO;IAAE,MAAM;IAAO,WAAW;GAAW,CAAC;EAEhF,EACF;CACF;AACF,CAAC;;;;ACzCD,MAAM,mCAAmB,IAAI,IAAI,CAAC,SAAS,UAAU,CAAC;;AAGtD,MAAa,wBAAwB,WAAW;CAC9C,MAAM;EACJ,MAAM;EACN,MAAM,EAAE,aAAa,gEAAgE;EACrF,UAAU,EACR,iBAAiB,qEACnB;CACF;CACA,WAAW,SAAS;EAClB,OAAO;GACL,kBAAkB,MAAM;IACtB,IAAI,KAAK,KAAK,SAAS,uBAAuB;IAC9C,IAAI,CAAC,iBAAiB,IAAI,KAAK,KAAK,SAAS,IAAI,GAAG;IACpD,KAAK,MAAM,aAAa,KAAK,YAC3B,IACE,UAAU,SAAS,kBACnB,UAAU,KAAK,SAAS,mBACxB,UAAU,KAAK,SAAS,cAExB,QAAQ,OAAO;KAAE,MAAM;KAAW,WAAW;IAAkB,CAAC;GAGtE;GACA,eAAe,MAAM;IACnB,IAAI,KAAK,OAAO,SAAS,gBAAgB,KAAK,OAAO,SAAS,YAAY;IAC1E,MAAM,CAAC,WAAW,KAAK;IACvB,IAAI,SAAS,SAAS,oBAAoB;IAC1C,MAAM,aAAa,cAAc,SAAS,YAAY;IACtD,IAAI,eAAe,MAAM,QAAQ,OAAO;KAAE,MAAM;KAAY,WAAW;IAAkB,CAAC;GAC5F;EACF;CACF;AACF,CAAC;;;;AClCD,MAAM,mCAAmB,IAAI,IAAI;CAAC;CAAM;CAAM;AAAI,CAAC;AAEnD,SAAS,SAAS,MAA4B;CAC5C,IAAI,KAAK,SAAS,iBAAiB,KAAK,SAAS,yBAAyB,OAAO;CACjF,OAAO,KAAK,SAAS,uBAAuB,iBAAiB,IAAI,KAAK,QAAQ;AAChF;;;;;AAMA,MAAa,wBAAwB,WAAW;CAC9C,MAAM;EACJ,MAAM;EACN,MAAM,EAAE,aAAa,uDAAuD;EAC5E,UAAU,EACR,QACE,0GACJ;CACF;CACA,WAAW,SAAS;EAClB,OAAO,EACL,eAAe,MAAM;GACnB,MAAM,UAAU,YAAY,IAAI;GAChC,MAAM,SAAS,YAAY,OAAO,OAAO,cAAc,SAAS,UAAU;GAC1E,IAAI,WAAW,QAAQ,iBAAiB,QAAQ,YAAY,MAAM,GAAG;GACrE,KAAK,MAAM,SAAS,YAAY,OAAO,KAAK,GAC1C,IAAI,SAAS,KAAK,GAAG,QAAQ,OAAO;IAAE,MAAM;IAAO,WAAW;GAAS,CAAC;EAE5E,EACF;CACF;AACF,CAAC;;;;ACnCD,MAAM,gCAAgB,IAAI,IAAI;CAAC;CAAgB;CAAgB;CAAiB;AAAiB,CAAC;;;;;AAMlG,MAAa,0BAA0B,WAAW;CAChD,MAAM;EACJ,MAAM;EACN,MAAM,EAAE,aAAa,kEAAkE;EACvF,UAAU,EACR,SACE,4HACJ;CACF;CACA,WAAW,SAAS;EAClB,MAAM,kBAAkB,QAAQ,SAAS,SAAS,aAAa;EAC/D,OAAO;GACL,SAAS,MAAM;IACb,IAAI,UAAU,KAAK,KAAK,YAAY,KAAK,IAAI,SAAS,cAAc;IACpE,IAAI,KAAK,IAAI,SAAS,cAAc,KAAK,MAAM,SAAS,mBACtD,QAAQ,OAAO;KAAE,MAAM,KAAK;KAAO,WAAW;IAAU,CAAC;GAE7D;GACA,eAAe,MAAM;IACnB,IAAI,UAAU,KAAK,KAAK,OAAO,SAAS,oBAAoB;IAC5D,MAAM,SAAS,KAAK,OAAO;IAC3B,IAAI,OAAO,SAAS,gBAAgB,CAAC,cAAc,IAAI,OAAO,IAAI,GAAG;IACrE,MAAM,CAAC,OAAO,KAAK;IACnB,IAAI,KAAK,SAAS,mBAAmB,QAAQ,OAAO;KAAE,MAAM;KAAK,WAAW;IAAU,CAAC;GACzF;EACF;CACF;AACF,CAAC;;;;;;;;;ACtBD,MAAM,iBAAiB,mBAAmB;CACxC,MAAM,EAAE,MAAM,WAAW;CACzB,OAAO;EACL,0BAA0B;EAC1B,8BAA8B;EAC9B,uBAAuB;EACvB,uBAAuB;EACvB,0BAA0B;CAC5B;AACF,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "better-ship",
4
- "version": "0.35.0",
4
+ "version": "0.36.0",
5
5
  "description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -64,6 +64,7 @@
64
64
  "./stripe/payment-webhook": "./dist/stripe/payment-webhook.js",
65
65
  "./stripe/request": "./dist/stripe/request.js",
66
66
  "./tanstack": "./dist/tanstack.js",
67
+ "./tanstack/oxlint": "./dist/tanstack/oxlint.js",
67
68
  "./tanstack/server": "./dist/tanstack/server.js",
68
69
  "./testing/outbox-repository": "./dist/testing/outbox-repository.js",
69
70
  "./testing/payment-service": "./dist/testing/payment-service.js",
@@ -87,9 +88,11 @@
87
88
  "@mdn/browser-compat-data": "^8.1.4",
88
89
  "@oxlint/plugins": "^1.86.0",
89
90
  "@posthog/react": "^1.11.3",
90
- "@sentry/cloudflare": "^11.2.0",
91
- "@sentry/tanstackstart-react": "^11.2.0",
91
+ "@sentry/cloudflare": "^11.6.0",
92
+ "@sentry/tanstackstart-react": "^11.6.0",
92
93
  "@tailwindcss/node": "^4.3.3",
94
+ "@tanstack/react-form": "^1.33.5",
95
+ "@tanstack/react-query": "^5.104.1",
93
96
  "@tanstack/react-router": "^1.170.41",
94
97
  "@tanstack/react-start": "^1.168.60",
95
98
  "@tanstack/start-client-core": "^1.170.34",
@@ -121,8 +124,8 @@
121
124
  "@mdn/browser-compat-data": "^8.1.4",
122
125
  "@oxlint/plugins": "^1.86.0",
123
126
  "@posthog/react": "^1.11.3",
124
- "@sentry/cloudflare": "^11.2.0",
125
- "@sentry/tanstackstart-react": "^11.2.0",
127
+ "@sentry/cloudflare": "^11.6.0",
128
+ "@sentry/tanstackstart-react": "^11.6.0",
126
129
  "@tanstack/react-router": "^1.170.41",
127
130
  "@tanstack/react-start": "^1.168.60",
128
131
  "better-auth": "^1.7.7",
@@ -0,0 +1,58 @@
1
+ import type { ESTree, SourceCode } from '@oxlint/plugins'
2
+
3
+ const FORM_HOOKS = new Set(['useForm', 'useAppForm'])
4
+ const SAFETY = /(?:^|[^\p{L}\p{N}_])SAFETY\s*:\s*\S/u
5
+
6
+ /** The options object of a `useForm({ … })` or `useAppForm({ … })` call. */
7
+ export function formOptions(node: ESTree.CallExpression): ESTree.ObjectExpression | null {
8
+ if (node.callee.type !== 'Identifier' || !FORM_HOOKS.has(node.callee.name)) return null
9
+ const [options] = node.arguments
10
+ return options?.type === 'ObjectExpression' ? options : null
11
+ }
12
+
13
+ /** A plain `name: value` property of an object literal. */
14
+ export function propertyNamed(
15
+ object: ESTree.ObjectExpression,
16
+ name: string,
17
+ ): ESTree.ObjectProperty | null {
18
+ for (const property of object.properties) {
19
+ if (property.type !== 'Property' || property.computed) continue
20
+ const key = property.key
21
+ if (key.type === 'Identifier' && key.name === name) return property
22
+ if (key.type === 'Literal' && key.value === name) return property
23
+ }
24
+ return null
25
+ }
26
+
27
+ /** A `// SAFETY:` comment directly before the node allows one exception. */
28
+ export function hasSafetyComment(sourceCode: SourceCode, node: ESTree.Node): boolean {
29
+ return sourceCode.getCommentsBefore(node).some((comment) => SAFETY.test(comment.value))
30
+ }
31
+
32
+ /** Every node under `root`. Parent links are not followed. */
33
+ export function* descendants(root: ESTree.Node): Generator<ESTree.Node> {
34
+ for (const [key, value] of Object.entries(root)) {
35
+ if (key === 'parent') continue
36
+ const children = Array.isArray(value) ? value : [value]
37
+ for (const child of children) {
38
+ if (isAstNode(child)) {
39
+ yield child
40
+ yield* descendants(child)
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ type AstChild =
47
+ | { type?: string; start?: number }
48
+ | string
49
+ | number
50
+ | boolean
51
+ | bigint
52
+ | symbol
53
+ | null
54
+ | undefined
55
+
56
+ function isAstNode(value: AstChild): value is ESTree.Node {
57
+ return value instanceof Object && 'type' in value && 'start' in value
58
+ }
@@ -0,0 +1,57 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+
3
+ import { formOptions, hasSafetyComment, propertyNamed } from './form-options.ts'
4
+
5
+ const DYNAMIC_SLOTS = new Set(['onDynamic', 'onDynamicAsync'])
6
+
7
+ /**
8
+ * Require one named schema on `useForm` and `useAppForm`.
9
+ * `validationLogic` must call `revalidateLogic`. The schema sits on `validators.onDynamic`.
10
+ * A `// SAFETY:` comment before the call allows one form.
11
+ */
12
+ export const formSchemaValidatorsRule = defineRule({
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ description:
17
+ 'Require `validationLogic: revalidateLogic(…)` and `validators: { onDynamic: Schema }` on every TanStack form.',
18
+ },
19
+ messages: {
20
+ logic:
21
+ 'Choose the timing with validationLogic: revalidateLogic({ mode, modeAfterSubmission }).',
22
+ missing: 'Validate the form with one schema: validators: { onDynamic: Schema }.',
23
+ slot: 'Replace {{slot}} with onDynamic; revalidateLogic sets the timing.',
24
+ inline: 'Pass a schema, not a function: validators: { onDynamic: Schema }.',
25
+ },
26
+ },
27
+ createOnce(context) {
28
+ return {
29
+ CallExpression(node) {
30
+ const options = formOptions(node)
31
+ if (options === null || hasSafetyComment(context.sourceCode, node)) return
32
+ const logic = propertyNamed(options, 'validationLogic')
33
+ if (
34
+ logic === null ||
35
+ logic.value.type !== 'CallExpression' ||
36
+ logic.value.callee.type !== 'Identifier' ||
37
+ logic.value.callee.name !== 'revalidateLogic'
38
+ ) {
39
+ context.report({ node: logic ?? node, messageId: 'logic' })
40
+ }
41
+ const validators = propertyNamed(options, 'validators')
42
+ if (validators === null || validators.value.type !== 'ObjectExpression') {
43
+ context.report({ node: validators ?? node, messageId: 'missing' })
44
+ return
45
+ }
46
+ for (const slot of validators.value.properties) {
47
+ if (slot.type !== 'Property' || slot.key.type !== 'Identifier') continue
48
+ if (!DYNAMIC_SLOTS.has(slot.key.name)) {
49
+ context.report({ node: slot, messageId: 'slot', data: { slot: slot.key.name } })
50
+ } else if (slot.value.type !== 'Identifier' && slot.value.type !== 'MemberExpression') {
51
+ context.report({ node: slot.value, messageId: 'inline' })
52
+ }
53
+ }
54
+ },
55
+ }
56
+ },
57
+ })
@@ -0,0 +1,46 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import type { ESTree } from '@oxlint/plugins'
3
+
4
+ import { descendants, formOptions, propertyNamed } from './form-options.ts'
5
+
6
+ /** A call whose result changes between renders, such as the current time or a random id. */
7
+ function isUnstable(node: ESTree.Node): boolean {
8
+ if (node.type === 'NewExpression') {
9
+ return node.callee.type === 'Identifier' && node.callee.name === 'Date'
10
+ }
11
+ if (node.type !== 'CallExpression' || node.callee.type !== 'MemberExpression') return false
12
+ const { object, property } = node.callee
13
+ if (object.type !== 'Identifier' || property.type !== 'Identifier') return false
14
+ return (
15
+ (object.name === 'Date' && property.name === 'now') ||
16
+ (object.name === 'Math' && property.name === 'random') ||
17
+ (object.name === 'crypto' && property.name === 'randomUUID')
18
+ )
19
+ }
20
+
21
+ /**
22
+ * Reject a time or random value written inside `defaultValues`.
23
+ * TanStack Form applies a changed default to an untouched form on the next render.
24
+ */
25
+ export const formStableDefaultValuesRule = defineRule({
26
+ meta: {
27
+ type: 'problem',
28
+ docs: { description: 'Disallow time or random values computed inline in `defaultValues`.' },
29
+ messages: {
30
+ unstable:
31
+ 'Compute this default once with useState(() => …) or a module constant; an inline value resets the form on render.',
32
+ },
33
+ },
34
+ createOnce(context) {
35
+ return {
36
+ CallExpression(node) {
37
+ const options = formOptions(node)
38
+ const defaults = options === null ? null : propertyNamed(options, 'defaultValues')
39
+ if (defaults === null) return
40
+ for (const child of [defaults.value, ...descendants(defaults.value)]) {
41
+ if (isUnstable(child)) context.report({ node: child, messageId: 'unstable' })
42
+ }
43
+ },
44
+ }
45
+ },
46
+ })
@@ -0,0 +1,25 @@
1
+ import { eslintCompatPlugin } from '@oxlint/plugins'
2
+
3
+ import { formSchemaValidatorsRule } from './form-schema-validators.ts'
4
+ import { formStableDefaultValuesRule } from './form-stable-default-values.ts'
5
+ import { noFieldValidatorsRule } from './no-field-validators.ts'
6
+ import { noSubmitBranchingRule } from './no-submit-branching.ts'
7
+ import { queryKeyFromFactoryRule } from './query-key-from-factory.ts'
8
+
9
+ /**
10
+ * Oxlint plugin for TanStack Form and Query.
11
+ * Enable the `tanstack/*` rules from `jsPlugins`.
12
+ * Ported from Suparelay `packages/tanstack-lint`. The rule ids are unchanged.
13
+ */
14
+ const tanstackPlugin = eslintCompatPlugin({
15
+ meta: { name: 'tanstack' },
16
+ rules: {
17
+ 'form-schema-validators': formSchemaValidatorsRule,
18
+ 'form-stable-default-values': formStableDefaultValuesRule,
19
+ 'no-field-validators': noFieldValidatorsRule,
20
+ 'no-submit-branching': noSubmitBranchingRule,
21
+ 'query-key-from-factory': queryKeyFromFactoryRule,
22
+ },
23
+ })
24
+
25
+ export default tanstackPlugin
@@ -0,0 +1,40 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+
3
+ import { propertyNamed } from './form-options.ts'
4
+
5
+ const FIELD_COMPONENTS = new Set(['Field', 'AppField'])
6
+
7
+ /** Reject field-level `validators`. The form schema is the only validation source. */
8
+ export const noFieldValidatorsRule = defineRule({
9
+ meta: {
10
+ type: 'problem',
11
+ docs: { description: 'Disallow field-level `validators`; the form schema validates.' },
12
+ messages: {
13
+ fieldValidators: 'Remove validators from the field. Put the rule in the form schema.',
14
+ },
15
+ },
16
+ createOnce(context) {
17
+ return {
18
+ JSXOpeningElement(node) {
19
+ if (node.name.type !== 'JSXMemberExpression') return
20
+ if (!FIELD_COMPONENTS.has(node.name.property.name)) return
21
+ for (const attribute of node.attributes) {
22
+ if (
23
+ attribute.type === 'JSXAttribute' &&
24
+ attribute.name.type === 'JSXIdentifier' &&
25
+ attribute.name.name === 'validators'
26
+ ) {
27
+ context.report({ node: attribute, messageId: 'fieldValidators' })
28
+ }
29
+ }
30
+ },
31
+ CallExpression(node) {
32
+ if (node.callee.type !== 'Identifier' || node.callee.name !== 'useField') return
33
+ const [options] = node.arguments
34
+ if (options?.type !== 'ObjectExpression') return
35
+ const validators = propertyNamed(options, 'validators')
36
+ if (validators !== null) context.report({ node: validators, messageId: 'fieldValidators' })
37
+ },
38
+ }
39
+ },
40
+ })
@@ -0,0 +1,38 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import type { ESTree } from '@oxlint/plugins'
3
+
4
+ import { descendants, formOptions, hasSafetyComment, propertyNamed } from './form-options.ts'
5
+
6
+ const BRANCH_OPERATORS = new Set(['??', '||', '&&'])
7
+
8
+ function isBranch(node: ESTree.Node): boolean {
9
+ if (node.type === 'IfStatement' || node.type === 'ConditionalExpression') return true
10
+ return node.type === 'LogicalExpression' && BRANCH_OPERATORS.has(node.operator)
11
+ }
12
+
13
+ /**
14
+ * Reject a condition inside `onSubmit`.
15
+ * The schema already accepted the values. A `// SAFETY:` comment on `onSubmit` allows one check.
16
+ */
17
+ export const noSubmitBranchingRule = defineRule({
18
+ meta: {
19
+ type: 'problem',
20
+ docs: { description: "Disallow conditions in a TanStack form's `onSubmit`." },
21
+ messages: {
22
+ branch:
23
+ 'Move this check into the form schema. Use Schema.parse(value) when the submitted type must be narrowed.',
24
+ },
25
+ },
26
+ createOnce(context) {
27
+ return {
28
+ CallExpression(node) {
29
+ const options = formOptions(node)
30
+ const submit = options === null ? null : propertyNamed(options, 'onSubmit')
31
+ if (submit === null || hasSafetyComment(context.sourceCode, submit)) return
32
+ for (const child of descendants(submit.value)) {
33
+ if (isBranch(child)) context.report({ node: child, messageId: 'branch' })
34
+ }
35
+ },
36
+ }
37
+ },
38
+ })
@@ -0,0 +1,36 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+
3
+ const CACHE_METHODS = new Set(['getQueryData', 'setQueryData', 'getQueryState', 'ensureQueryData'])
4
+
5
+ /**
6
+ * Reject a query key array written outside a `*-queries.ts` factory.
7
+ * Invalidation then reads the same key the factory stored.
8
+ */
9
+ export const queryKeyFromFactoryRule = defineRule({
10
+ meta: {
11
+ type: 'problem',
12
+ docs: { description: 'Disallow query key arrays outside `*-queries.ts` factory files.' },
13
+ messages: {
14
+ literal:
15
+ "Read this key from the entity's query factory, for example entityQueries.lists() or entityQueries.detail(input).queryKey.",
16
+ },
17
+ },
18
+ createOnce(context) {
19
+ const inFactory = () => context.filename.endsWith('-queries.ts')
20
+ return {
21
+ Property(node) {
22
+ if (inFactory() || node.computed || node.key.type !== 'Identifier') return
23
+ if (node.key.name === 'queryKey' && node.value.type === 'ArrayExpression') {
24
+ context.report({ node: node.value, messageId: 'literal' })
25
+ }
26
+ },
27
+ CallExpression(node) {
28
+ if (inFactory() || node.callee.type !== 'MemberExpression') return
29
+ const method = node.callee.property
30
+ if (method.type !== 'Identifier' || !CACHE_METHODS.has(method.name)) return
31
+ const [key] = node.arguments
32
+ if (key?.type === 'ArrayExpression') context.report({ node: key, messageId: 'literal' })
33
+ },
34
+ }
35
+ },
36
+ })