@ingram-tech/nk-dev 0.4.1 → 0.5.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,109 @@
|
|
|
1
|
+
// nextkit oxlint JS plugin rule: catch `event.currentTarget` reads that
|
|
2
|
+
// outlive the event handler.
|
|
3
|
+
//
|
|
4
|
+
// The trap: React nulls a synthetic event's `currentTarget` once the handler
|
|
5
|
+
// returns — it is per-dispatch state, reassigned as the one event object
|
|
6
|
+
// traverses the propagation path (mirroring the DOM spec, where currentTarget
|
|
7
|
+
// is only defined during dispatch). Reading it inside a callback that runs
|
|
8
|
+
// after the handler — a functional setState updater, setTimeout, a promise
|
|
9
|
+
// chain, a debounced closure — crashes with "Cannot read properties of null".
|
|
10
|
+
// The failure is intermittent: React evaluates a setState updater eagerly when
|
|
11
|
+
// the queue is empty, so the first keystroke works and only the replayed or
|
|
12
|
+
// queued case crashes.
|
|
13
|
+
//
|
|
14
|
+
// tsc CANNOT catch this class: @types/react declares `currentTarget` non-null
|
|
15
|
+
// (true during dispatch), and the nulling is a temporal invariant the type
|
|
16
|
+
// system cannot express. Worse, `currentTarget` is the better-typed accessor
|
|
17
|
+
// (typed as the element the handler is attached to, unlike `target`), so
|
|
18
|
+
// TS-first code is steered toward exactly the property that expires. React 16
|
|
19
|
+
// warned at runtime on any post-handler event access (event pooling); React 17
|
|
20
|
+
// removed pooling and the warning with it, leaving `currentTarget` as the one
|
|
21
|
+
// silently expiring property. Hence a lint rule.
|
|
22
|
+
//
|
|
23
|
+
// Detection: report `x.currentTarget` where `x` is bound as a parameter by an
|
|
24
|
+
// ENCLOSING function other than the innermost one — i.e. the read crosses a
|
|
25
|
+
// closure boundary out of the handler. Locals declared in the current function
|
|
26
|
+
// (including a captured `const target = event.currentTarget` in the handler
|
|
27
|
+
// body, which is the fix) never report.
|
|
28
|
+
//
|
|
29
|
+
// Known false positive: a closure the handler invokes synchronously itself
|
|
30
|
+
// (e.g. `items.map((it) => event.currentTarget...)`). That pattern is rare and
|
|
31
|
+
// fragile anyway; prefer capturing first, or suppress with a justified
|
|
32
|
+
// oxlint-disable comment.
|
|
33
|
+
|
|
34
|
+
const collectParamBindings = (params, into) => {
|
|
35
|
+
for (const param of params) {
|
|
36
|
+
if (param.type === "Identifier") into.add(param.name);
|
|
37
|
+
else if (
|
|
38
|
+
param.type === "AssignmentPattern" &&
|
|
39
|
+
param.left.type === "Identifier"
|
|
40
|
+
) {
|
|
41
|
+
into.add(param.left.name);
|
|
42
|
+
} else if (
|
|
43
|
+
param.type === "RestElement" &&
|
|
44
|
+
param.argument.type === "Identifier"
|
|
45
|
+
) {
|
|
46
|
+
into.add(param.argument.name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const noDeferredCurrentTarget = {
|
|
52
|
+
meta: {
|
|
53
|
+
type: "problem",
|
|
54
|
+
docs: {
|
|
55
|
+
description:
|
|
56
|
+
"Disallow reading `event.currentTarget` inside a callback nested in the event handler; React nulls `currentTarget` after dispatch.",
|
|
57
|
+
},
|
|
58
|
+
messages: {
|
|
59
|
+
deferred:
|
|
60
|
+
"`{{name}}.currentTarget` is read inside a callback nested in the event handler. React nulls `currentTarget` once the handler returns, so this can crash when the callback runs later (e.g. a replayed setState updater). Capture the value into a local in the handler body and use that instead.",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
create(context) {
|
|
64
|
+
const scopes = [];
|
|
65
|
+
const enterFunction = (node) => {
|
|
66
|
+
const bindings = new Set();
|
|
67
|
+
collectParamBindings(node.params, bindings);
|
|
68
|
+
scopes.push(bindings);
|
|
69
|
+
};
|
|
70
|
+
const exitFunction = () => {
|
|
71
|
+
scopes.pop();
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
FunctionDeclaration: enterFunction,
|
|
75
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
76
|
+
FunctionExpression: enterFunction,
|
|
77
|
+
"FunctionExpression:exit": exitFunction,
|
|
78
|
+
ArrowFunctionExpression: enterFunction,
|
|
79
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
80
|
+
VariableDeclarator(node) {
|
|
81
|
+
// Track locals so a variable declared in the current function
|
|
82
|
+
// (including a rebound `event`) never reports.
|
|
83
|
+
if (scopes.length > 0 && node.id.type === "Identifier") {
|
|
84
|
+
scopes[scopes.length - 1].add(node.id.name);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
MemberExpression(node) {
|
|
88
|
+
if (node.computed) return;
|
|
89
|
+
if (
|
|
90
|
+
node.property.type !== "Identifier" ||
|
|
91
|
+
node.property.name !== "currentTarget"
|
|
92
|
+
) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (node.object.type !== "Identifier") return;
|
|
96
|
+
if (scopes.length < 2) return;
|
|
97
|
+
const name = node.object.name;
|
|
98
|
+
if (scopes[scopes.length - 1].has(name)) return;
|
|
99
|
+
if (!scopes.slice(0, -1).some((frame) => frame.has(name))) return;
|
|
100
|
+
context.report({ node, messageId: "deferred", data: { name } });
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default {
|
|
107
|
+
meta: { name: "nextkit" },
|
|
108
|
+
rules: { "no-deferred-current-target": noDeferredCurrentTarget },
|
|
109
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The `nextkit` oxlint JS plugin: one rule per file, merged here. This is the
|
|
2
|
+
// module `@ingram-tech/nk-dev/oxlint-plugin` resolves to and the shared
|
|
3
|
+
// oxlintrc.json loads via `jsPlugins`.
|
|
4
|
+
|
|
5
|
+
import baseUi from "./base-ui.js";
|
|
6
|
+
import deferredCurrentTarget from "./deferred-current-target.js";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
meta: { name: "nextkit" },
|
|
10
|
+
rules: { ...baseUi.rules, ...deferredCurrentTarget.rules },
|
|
11
|
+
};
|
package/oxlintrc.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-tech/nk-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The nextkit dev toolchain in one package: the `nk` CLI plus shared oxlint/oxfmt, TypeScript, and Vitest config, the format-on-commit hook, and the AI agent guide. `nk init` scaffolds a site to use it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"./oxlintrc.json": "./oxlintrc.json",
|
|
31
31
|
"./oxfmtrc.json": "./oxfmtrc.json",
|
|
32
32
|
"./tier-b.json": "./tier-b.json",
|
|
33
|
-
"./oxlint-plugin": "./lib/oxlint-plugins/
|
|
33
|
+
"./oxlint-plugin": "./lib/oxlint-plugins/index.js",
|
|
34
34
|
"./tsconfig": "./tsconfig/nextjs.json",
|
|
35
35
|
"./tsconfig/base.json": "./tsconfig/base.json",
|
|
36
36
|
"./tsconfig/nextjs.json": "./tsconfig/nextjs.json",
|