@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
@@ -6,6 +6,7 @@
6
6
  },
7
7
  "rules": {
8
8
  "nextkit/no-radix-props-on-base-ui": "error",
9
+ "nextkit/no-deferred-current-target": "error",
9
10
  "no-unused-vars": "warn",
10
11
  "typescript/no-non-null-assertion": "error",
11
12
  "typescript/no-explicit-any": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/nk-dev",
3
- "version": "0.4.1",
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/base-ui.js",
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",