@ilha/router 0.11.3 → 0.11.4

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.
package/oxlint.cjs CHANGED
@@ -11,49 +11,81 @@ const CONDITIONAL = new Set([
11
11
  "ConditionalExpression",
12
12
  "LogicalExpression",
13
13
  ]);
14
- const FN_TYPES = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
14
+ const FN_TYPES = new Set([
15
+ "FunctionDeclaration",
16
+ "FunctionExpression",
17
+ "ArrowFunctionExpression",
18
+ ]);
19
+
20
+ const objectTag = (value) => Object.prototype.toString.call(value);
21
+ const isFunction = (value) => {
22
+ const tag = objectTag(value);
23
+ return (
24
+ tag === "[object Function]" ||
25
+ tag === "[object AsyncFunction]" ||
26
+ tag === "[object GeneratorFunction]"
27
+ );
28
+ };
15
29
 
16
- function bindings() {
17
- return { prim: new Map() };
18
- }
30
+ const bindings = () => ({ prim: new Map() });
19
31
 
20
- function takeImport(b, node) {
21
- if (node.source.value !== "ilha") return;
32
+ const takeImport = (b, node) => {
33
+ if (node.source.value !== "ilha") {
34
+ return;
35
+ }
22
36
  for (const s of node.specifiers) {
23
- if (s.type !== "ImportSpecifier") continue;
24
- if (PRIMITIVES.has(s.imported.name)) b.prim.set(s.local.name, s.imported.name);
37
+ if (s.type !== "ImportSpecifier") {
38
+ continue;
39
+ }
40
+ if (PRIMITIVES.has(s.imported.name)) {
41
+ b.prim.set(s.local.name, s.imported.name);
42
+ }
25
43
  }
26
- }
44
+ };
27
45
 
28
- function primitiveName(node, b) {
29
- if (node.type !== "CallExpression") return null;
30
- if (node.callee.type !== "Identifier") return null;
46
+ const primitiveName = (node, b) => {
47
+ if (node.type !== "CallExpression") {
48
+ return null;
49
+ }
50
+ if (node.callee.type !== "Identifier") {
51
+ return null;
52
+ }
31
53
  return b.prim.get(node.callee.name) ?? null;
32
- }
54
+ };
33
55
 
34
- function ancestorsOf(context, node) {
56
+ const ancestorsOf = (context, node) => {
35
57
  const sc = context.sourceCode;
36
- if (sc && typeof sc.getAncestors === "function") return sc.getAncestors(node);
58
+ if (sc && isFunction(sc.getAncestors)) {
59
+ return sc.getAncestors(node);
60
+ }
37
61
  return [];
38
- }
62
+ };
39
63
 
40
- function blockBody(node) {
41
- if (node.type === "BlockStatement") return node.body;
64
+ const blockBody = (node) => {
65
+ if (node.type === "BlockStatement") {
66
+ return node.body;
67
+ }
42
68
  if (
43
69
  node.type === "FunctionDeclaration" ||
44
70
  node.type === "FunctionExpression" ||
45
71
  node.type === "ArrowFunctionExpression"
46
72
  ) {
47
- const body = node.body;
48
- if (body?.type === "BlockStatement") return body.body;
73
+ const { body } = node;
74
+ if (body?.type === "BlockStatement") {
75
+ return body.body;
76
+ }
49
77
  }
50
78
  return null;
51
- }
79
+ };
52
80
 
53
- function bindingInitType(name, ancestors) {
54
- for (let i = ancestors.length - 1; i >= 0; i--) {
81
+ const bindingInitType = (name, ancestors) => {
82
+ for (let i = ancestors.length - 1; i >= 0; i -= 1) {
55
83
  const a = ancestors[i];
56
- if (a.type === "VariableDeclarator" && a.id?.type === "Identifier" && a.id.name === name) {
84
+ if (
85
+ a.type === "VariableDeclarator" &&
86
+ a.id?.type === "Identifier" &&
87
+ a.id.name === name
88
+ ) {
57
89
  return a.init?.type ?? "none";
58
90
  }
59
91
  if (a.type === "FunctionDeclaration" && a.id?.name === name) {
@@ -62,7 +94,9 @@ function bindingInitType(name, ancestors) {
62
94
  const stmts = blockBody(a);
63
95
  if (stmts) {
64
96
  for (const stmt of stmts) {
65
- if (stmt.type !== "VariableDeclaration") continue;
97
+ if (stmt.type !== "VariableDeclaration") {
98
+ continue;
99
+ }
66
100
  for (const decl of stmt.declarations) {
67
101
  if (decl.id?.type === "Identifier" && decl.id.name === name) {
68
102
  return decl.init?.type ?? "none";
@@ -72,223 +106,268 @@ function bindingInitType(name, ancestors) {
72
106
  }
73
107
  }
74
108
  return null;
75
- }
109
+ };
76
110
 
77
- function isFunctionBinding(name, ancestors, moduleFns) {
111
+ const isFunctionBinding = (name, ancestors, moduleFns) => {
78
112
  const local = bindingInitType(name, ancestors);
79
- if (local === "FunctionDeclaration") return true;
80
- if (local !== null) return FN_TYPES.has(local);
113
+ if (local === "FunctionDeclaration") {
114
+ return true;
115
+ }
116
+ if (local !== null) {
117
+ return FN_TYPES.has(local);
118
+ }
81
119
  return moduleFns.has(name);
82
- }
120
+ };
83
121
 
84
- function enclosingFunctions(ancestors) {
85
- return ancestors.filter((a) => FN_TYPES.has(a.type));
86
- }
122
+ const enclosingFunctions = (ancestors) =>
123
+ ancestors.filter((a) => FN_TYPES.has(a.type));
87
124
 
88
125
  const noConditionalPrimitive = {
89
- meta: {
90
- type: "problem",
91
- docs: { description: "Disallow atom/watch/when inside conditionals or loops" },
92
- messages: {
93
- conditional:
94
- "Do not call {{name}}() inside a condition or loop. Primitive registration order must be stable — put the branch inside the primitive or the view.",
95
- },
96
- schema: [],
97
- },
98
126
  create(context) {
99
127
  const b = bindings();
100
128
  return {
101
- ImportDeclaration(node) {
102
- takeImport(b, node);
103
- },
104
129
  CallExpression(node) {
105
130
  const name = primitiveName(node, b);
106
- if (!name) return;
131
+ if (!name) {
132
+ return;
133
+ }
107
134
  const ancestors = ancestorsOf(context, node);
108
- for (let i = ancestors.length - 1; i >= 0; i--) {
135
+ for (let i = ancestors.length - 1; i >= 0; i -= 1) {
109
136
  const a = ancestors[i];
110
- if (FN_TYPES.has(a.type)) return;
137
+ if (FN_TYPES.has(a.type)) {
138
+ return;
139
+ }
111
140
  if (CONDITIONAL.has(a.type)) {
112
- context.report({ node, messageId: "conditional", data: { name } });
141
+ context.report({ data: { name }, messageId: "conditional", node });
113
142
  return;
114
143
  }
115
144
  }
116
145
  },
146
+ ImportDeclaration(node) {
147
+ takeImport(b, node);
148
+ },
117
149
  };
118
150
  },
119
- };
120
-
121
- const noPrimitiveOutsideComponent = {
122
151
  meta: {
123
- type: "problem",
124
152
  docs: {
125
- description: "Call atom/watch/when inside a component, never at module top level",
153
+ description: "Disallow atom/watch/when inside conditionals or loops",
126
154
  },
127
155
  messages: {
128
- outside:
129
- "Call {{name}}() inside a component. There is no fiber at module scope, so this throws at runtime.",
156
+ conditional:
157
+ "Do not call {{name}}() inside a condition or loop. Primitive registration order must be stable — put the branch inside the primitive or the view.",
130
158
  },
131
159
  schema: [],
160
+ type: "problem",
132
161
  },
162
+ };
163
+
164
+ const noPrimitiveOutsideComponent = {
133
165
  create(context) {
134
166
  const b = bindings();
135
167
  return {
136
- ImportDeclaration(node) {
137
- takeImport(b, node);
138
- },
139
168
  CallExpression(node) {
140
169
  const name = primitiveName(node, b);
141
- if (!name) return;
170
+ if (!name) {
171
+ return;
172
+ }
142
173
  const ancestors = ancestorsOf(context, node);
143
174
  if (enclosingFunctions(ancestors).length === 0) {
144
- context.report({ node, messageId: "outside", data: { name } });
175
+ context.report({ data: { name }, messageId: "outside", node });
145
176
  }
146
177
  },
178
+ ImportDeclaration(node) {
179
+ takeImport(b, node);
180
+ },
147
181
  };
148
182
  },
149
- };
150
-
151
- const noInstructionOutsideGenerator = {
152
183
  meta: {
153
- type: "problem",
154
- docs: { description: "Use when only inside a generator, with yield*" },
184
+ docs: {
185
+ description:
186
+ "Call atom/watch/when inside a component, never at module top level",
187
+ },
155
188
  messages: {
156
- notGenerator:
157
- "{{name}}() returns an instruction that only a generator frame can run. Make the enclosing component a generator and yield* it.",
158
- notYielded: "Call yield* with {{name}}() — calling it directly discards the instruction.",
189
+ outside:
190
+ "Call {{name}}() inside a component. There is no fiber at module scope, so this throws at runtime.",
159
191
  },
160
192
  schema: [],
193
+ type: "problem",
161
194
  },
195
+ };
196
+
197
+ const noInstructionOutsideGenerator = {
162
198
  create(context) {
163
199
  const b = bindings();
164
200
  return {
165
- ImportDeclaration(node) {
166
- takeImport(b, node);
167
- },
168
201
  CallExpression(node) {
169
202
  const name = primitiveName(node, b);
170
- if (!name || !INSTRUCTIONS.has(name)) return;
203
+ if (!name || !INSTRUCTIONS.has(name)) {
204
+ return;
205
+ }
171
206
  // yield* wraps the CallExpression in a YieldExpression.
172
207
  const parent = ancestorsOf(context, node).at(-1);
173
- if (parent?.type === "YieldExpression") return;
208
+ if (parent?.type === "YieldExpression") {
209
+ return;
210
+ }
174
211
  const ancestors = ancestorsOf(context, node);
175
212
  const fns = enclosingFunctions(ancestors);
176
213
  const gen = fns.findLast((f) => f.generator === true);
177
214
  if (!gen) {
178
- context.report({ node, messageId: "notGenerator", data: { name } });
215
+ context.report({ data: { name }, messageId: "notGenerator", node });
179
216
  return;
180
217
  }
181
218
  // Inside a generator body but not under a yield — the nearest function
182
219
  // between the call and the generator must not be a plain callback that
183
220
  // escapes the frame. If the call sits directly in the generator, require yield*.
184
221
  const genIdx = ancestors.indexOf(gen);
185
- const between = ancestors.slice(genIdx + 1).filter((a) => FN_TYPES.has(a.type));
222
+ const between = ancestors
223
+ .slice(genIdx + 1)
224
+ .filter((a) => FN_TYPES.has(a.type));
186
225
  if (between.length === 0) {
187
- context.report({ node, messageId: "notYielded", data: { name } });
226
+ context.report({ data: { name }, messageId: "notYielded", node });
188
227
  }
189
228
  },
229
+ ImportDeclaration(node) {
230
+ takeImport(b, node);
231
+ },
190
232
  };
191
233
  },
192
- };
193
-
194
- const preferLowercaseEvents = {
195
234
  meta: {
196
- type: "problem",
197
- docs: { description: "Use lowercase DOM event props (onclick, not onClick)" },
235
+ docs: { description: "Use when only inside a generator, with yield*" },
198
236
  messages: {
199
- camel: 'Use lowercase event "{{suggested}}" instead of "{{name}}".',
237
+ notGenerator:
238
+ "{{name}}() returns an instruction that only a generator frame can run. Make the enclosing component a generator and yield* it.",
239
+ notYielded:
240
+ "Call yield* with {{name}}() — calling it directly discards the instruction.",
200
241
  },
201
242
  schema: [],
243
+ type: "problem",
202
244
  },
245
+ };
246
+
247
+ const preferLowercaseEvents = {
203
248
  create(context) {
204
- function checkName(node, name) {
205
- if (/^on[A-Z]/.test(name)) {
249
+ const checkName = (node, name) => {
250
+ if (/^on[A-Z]/u.test(name)) {
206
251
  context.report({
207
- node,
208
- messageId: "camel",
209
252
  data: { name, suggested: `on${name.slice(2).toLowerCase()}` },
253
+ messageId: "camel",
254
+ node,
210
255
  });
211
256
  }
212
- }
257
+ };
213
258
  return {
214
259
  JSXAttribute(node) {
215
- if (node.name.type === "JSXIdentifier") checkName(node.name, node.name.name);
260
+ if (node.name.type === "JSXIdentifier") {
261
+ checkName(node.name, node.name.name);
262
+ }
216
263
  if (node.name.type === "JSXNamespacedName") {
217
264
  checkName(node.name.namespace, node.name.namespace.name);
218
265
  }
219
266
  },
220
- "Property[value.type=/Function(Expression|Declaration)$/], Property[value.type='ArrowFunctionExpression']"() {},
267
+ // Selector reserved for future property-value checks; keep registered.
268
+ "Property[value.type=/Function(Expression|Declaration)$/], Property[value.type='ArrowFunctionExpression']"() {
269
+ void 0;
270
+ },
221
271
  };
222
272
  },
223
- };
224
-
225
- const functionInAtom = {
226
273
  meta: {
227
- type: "problem",
228
274
  docs: {
229
- description:
230
- "atom(fn) and atom(() => …) are invalid — use atom.lazy, Atom.map, or Atom.transform",
275
+ description: "Use lowercase DOM event props (onclick, not onClick)",
231
276
  },
232
277
  messages: {
233
- init: "atom({{name}}) is not valid. Use atom.lazy(() => {{name}}) to store a function value.",
234
- derived:
235
- "atom(() => …) is not supported. Use atom(Atom.map(...)) or atom(Atom.transform(...)) for derived values, or atom.lazy() for one-time init.",
278
+ camel: 'Use lowercase event "{{suggested}}" instead of "{{name}}".',
236
279
  },
237
280
  schema: [],
281
+ type: "problem",
238
282
  },
283
+ };
284
+
285
+ const functionInAtom = {
239
286
  create(context) {
240
287
  const b = bindings();
241
288
  const fns = new Set();
242
289
  return {
243
- ImportDeclaration(node) {
244
- takeImport(b, node);
290
+ CallExpression(node) {
291
+ if (node.arguments.length === 0) {
292
+ return;
293
+ }
294
+ const [arg] = node.arguments;
295
+ if (arg.type !== "Identifier") {
296
+ return;
297
+ }
298
+ if (
299
+ primitiveName(node, b) === "atom" &&
300
+ isFunctionBinding(arg.name, ancestorsOf(context, arg), fns)
301
+ ) {
302
+ context.report({
303
+ data: { name: arg.name },
304
+ messageId: "init",
305
+ node: arg,
306
+ });
307
+ }
245
308
  },
246
309
  FunctionDeclaration(node) {
247
- if (node.id) fns.add(node.id.name);
310
+ if (node.id) {
311
+ fns.add(node.id.name);
312
+ }
313
+ },
314
+ ImportDeclaration(node) {
315
+ takeImport(b, node);
248
316
  },
249
317
  VariableDeclarator(node) {
250
- if (node.id.type !== "Identifier") return;
251
- const init = node.init;
318
+ if (node.id.type !== "Identifier") {
319
+ return;
320
+ }
321
+ const { init } = node;
252
322
  if (
253
323
  init &&
254
- (init.type === "FunctionExpression" || init.type === "ArrowFunctionExpression")
324
+ (init.type === "FunctionExpression" ||
325
+ init.type === "ArrowFunctionExpression")
255
326
  ) {
256
327
  fns.add(node.id.name);
257
328
  }
258
- if (!init || primitiveName(init, b) !== "atom") return;
259
- const arg = init.arguments[0];
329
+ if (!init || primitiveName(init, b) !== "atom") {
330
+ return;
331
+ }
332
+ const [arg] = init.arguments;
260
333
  if (arg && FN_TYPES.has(arg.type)) {
261
- context.report({ node: arg, messageId: "derived" });
334
+ context.report({ messageId: "derived", node: arg });
262
335
  }
263
336
  if (
264
337
  arg?.type === "Identifier" &&
265
338
  isFunctionBinding(arg.name, ancestorsOf(context, arg), fns)
266
339
  ) {
267
- context.report({ node: arg, messageId: "init", data: { name: arg.name } });
268
- }
269
- },
270
- CallExpression(node) {
271
- if (node.arguments.length === 0) return;
272
- const arg = node.arguments[0];
273
- if (arg.type !== "Identifier") return;
274
- if (
275
- primitiveName(node, b) === "atom" &&
276
- isFunctionBinding(arg.name, ancestorsOf(context, arg), fns)
277
- ) {
278
- context.report({ node: arg, messageId: "init", data: { name: arg.name } });
340
+ context.report({
341
+ data: { name: arg.name },
342
+ messageId: "init",
343
+ node: arg,
344
+ });
279
345
  }
280
346
  },
281
347
  };
282
348
  },
349
+ meta: {
350
+ docs: {
351
+ description:
352
+ "atom(fn) and atom(() => …) are invalid — use atom.lazy, Atom.map, or Atom.transform",
353
+ },
354
+ messages: {
355
+ derived:
356
+ "atom(() => …) is not supported. Use atom(Atom.map(...)) or atom(Atom.transform(...)) for derived values, or atom.lazy() for one-time init.",
357
+ init: "atom({{name}}) is not valid. Use atom.lazy(() => {{name}}) to store a function value.",
358
+ },
359
+ schema: [],
360
+ type: "problem",
361
+ },
283
362
  };
284
363
 
285
364
  module.exports = {
286
365
  meta: { name: "oxlint-plugin-ilha" },
287
366
  rules: {
367
+ "function-in-atom": functionInAtom,
288
368
  "no-conditional-primitive": noConditionalPrimitive,
289
- "no-primitive-outside-component": noPrimitiveOutsideComponent,
290
369
  "no-instruction-outside-generator": noInstructionOutsideGenerator,
370
+ "no-primitive-outside-component": noPrimitiveOutsideComponent,
291
371
  "prefer-lowercase-events": preferLowercaseEvents,
292
- "function-in-atom": functionInAtom,
293
372
  },
294
373
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/router",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "keywords": [
6
6
  "frontend",
@@ -73,12 +73,12 @@
73
73
  "unplugin": "3.3.0"
74
74
  },
75
75
  "devDependencies": {
76
- "ilha": "^0.14.3",
76
+ "ilha": "^0.14.4",
77
77
  "oxidejs": "^0.3.0",
78
78
  "vite": "^8.2.2"
79
79
  },
80
80
  "peerDependencies": {
81
- "ilha": ">=0.14.3",
81
+ "ilha": ">=0.14.4",
82
82
  "oxidejs": ">=0.3.0"
83
83
  },
84
84
  "peerDependenciesMeta": {