@ilha/router 0.10.0 → 0.10.2
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/dist/{plugin-CmI3Brr2.js → plugin-Bh0y5kkI.js} +1 -1
- package/dist/rsbuild.js +1 -1
- package/dist/server.d.ts +8 -0
- package/dist/server.js +14 -0
- package/dist/{ssr-BxrcUYy5.js → ssr-CGcMUP1G.js} +4 -27
- package/dist/ssr.d.ts +4 -8
- package/dist/ssr.js +1 -1
- package/dist/vite.js +1 -1
- package/oxlint.cjs +428 -0
- package/package.json +11 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as setLoaderGuard, T as runWithIslandRequest, b as setFrameAuth, c as frameEnvelope, m as isSafeFramePath, n as FrameError, o as authorizeFrameRequest, u as getFrameGuard, x as setFrameGuard, y as renderServerIsland } from "./ssr-
|
|
1
|
+
import { C as setLoaderGuard, T as runWithIslandRequest, b as setFrameAuth, c as frameEnvelope, m as isSafeFramePath, n as FrameError, o as authorizeFrameRequest, u as getFrameGuard, x as setFrameGuard, y as renderServerIsland } from "./ssr-CGcMUP1G.js";
|
|
2
2
|
import { existsSync, readFileSync, statSync, watch } from "node:fs";
|
|
3
3
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { createUnplugin } from "unplugin";
|
package/dist/rsbuild.js
CHANGED
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "ilha";
|
|
2
|
+
import { type ServerAction } from "ilha/internal";
|
|
3
|
+
/**
|
|
4
|
+
* Declare an Oxide RPC action that can also be passed directly to server-island
|
|
5
|
+
* event props or bound to a serializable payload with `.with()`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function action<A extends unknown[], R>(fn: (...args: A) => R): ServerAction<A, R>;
|
|
8
|
+
export type { ServerAction } from "ilha/internal";
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "ilha";
|
|
2
|
+
import { bindServerAction } from "ilha/internal";
|
|
3
|
+
|
|
4
|
+
//#region src/server.ts
|
|
5
|
+
/**
|
|
6
|
+
* Declare an Oxide RPC action that can also be passed directly to server-island
|
|
7
|
+
* event props or bound to a serializable payload with `.with()`.
|
|
8
|
+
*/
|
|
9
|
+
function action(fn) {
|
|
10
|
+
return bindServerAction(fn);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { action };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { M as parsePattern, N as safeDecode, b as resolveRedirectTarget, j as matchSegments } from "./src-B5dHU24f.js";
|
|
2
|
-
import
|
|
2
|
+
import "ilha";
|
|
3
|
+
import { bindServerAction, setServerManifestSerializer } from "ilha/internal";
|
|
3
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
5
|
|
|
5
6
|
//#region src/request-scope.ts
|
|
@@ -34,24 +35,6 @@ function runWithIslandRequest(request, fn) {
|
|
|
34
35
|
|
|
35
36
|
//#endregion
|
|
36
37
|
//#region src/ssr.ts
|
|
37
|
-
/**
|
|
38
|
-
* Production SSR endpoints for server-owned islands and regular-page loads.
|
|
39
|
-
*
|
|
40
|
-
* Default export is an oxidejs-style fetch middleware:
|
|
41
|
-
* `(request) => Response | undefined`. Returns `undefined` for any request it
|
|
42
|
-
* does not own, so hosts can chain it ahead of their own handler:
|
|
43
|
-
*
|
|
44
|
-
* ```ts
|
|
45
|
-
* oxide({ middleware: ["@ilha/router/ssr"] });
|
|
46
|
-
* ```
|
|
47
|
-
*
|
|
48
|
-
* Serves:
|
|
49
|
-
* - `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }` in,
|
|
50
|
-
* `{ html }` out). Renderers come from the process-global registry
|
|
51
|
-
* populated by self-registration code appended to `.server` modules.
|
|
52
|
-
* - `GET /__ilha/loader?path=…` — regular-page server loads via the loader
|
|
53
|
-
* runner (`setFrameLoaderRunner`, wired by the generated server module).
|
|
54
|
-
*/
|
|
55
38
|
const REGISTRY_KEY = Symbol.for("ilha.serverIslandRenderers");
|
|
56
39
|
function registry() {
|
|
57
40
|
const g = globalThis;
|
|
@@ -191,15 +174,9 @@ function getFrameLoaderRunner() {
|
|
|
191
174
|
setServerManifestSerializer({ template(manifest) {
|
|
192
175
|
return `<template data-ilha-actions='${JSON.stringify(Object.fromEntries(manifest)).replace(/&/g, "&").replace(/'/g, "'").replace(/</g, "<")}'></template>`;
|
|
193
176
|
} });
|
|
194
|
-
/**
|
|
195
|
-
/**
|
|
196
|
-
* Wrap an exported server action with a named transport key. Ilha never
|
|
197
|
-
* executes event-handler closures during server rendering (fail closed), so
|
|
198
|
-
* this is a transparent passthrough kept for API compatibility; the `key`
|
|
199
|
-
* documents the RPC transport name used by tooling.
|
|
200
|
-
*/
|
|
177
|
+
/** @internal Brand an exported server action with its generated RPC transport key. */
|
|
201
178
|
function __ilhaServerAction(key, fn) {
|
|
202
|
-
return
|
|
179
|
+
return bindServerAction(fn, key);
|
|
203
180
|
}
|
|
204
181
|
function registerServerIsland(id, render, options) {
|
|
205
182
|
registry().set(id, {
|
package/dist/ssr.d.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* - `GET /__ilha/loader?path=…` — regular-page server loads via the loader
|
|
17
17
|
* runner (`setFrameLoaderRunner`, wired by the generated server module).
|
|
18
18
|
*/
|
|
19
|
+
import "ilha";
|
|
20
|
+
import { type ServerAction } from "ilha/internal";
|
|
19
21
|
import type { HeadInput } from "./head";
|
|
20
22
|
/**
|
|
21
23
|
* Server-frame state shared by the dev middleware and the production
|
|
@@ -143,14 +145,8 @@ export type FrameLoaderRunner = (path: string, request?: Request) => Promise<{
|
|
|
143
145
|
*/
|
|
144
146
|
export declare function setFrameLoaderRunner(runner: FrameLoaderRunner): void;
|
|
145
147
|
export declare function getFrameLoaderRunner(): FrameLoaderRunner | undefined;
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
* Wrap an exported server action with a named transport key. Ilha never
|
|
149
|
-
* executes event-handler closures during server rendering (fail closed), so
|
|
150
|
-
* this is a transparent passthrough kept for API compatibility; the `key`
|
|
151
|
-
* documents the RPC transport name used by tooling.
|
|
152
|
-
*/
|
|
153
|
-
export declare function __ilhaServerAction<A extends unknown[], R>(key: string, fn: (...args: A) => R): (...args: A) => R | undefined;
|
|
148
|
+
/** @internal Brand an exported server action with its generated RPC transport key. */
|
|
149
|
+
export declare function __ilhaServerAction<A extends unknown[], R>(key: string, fn: (...args: A) => R): ServerAction<A, R>;
|
|
154
150
|
export declare function registerServerIsland(id: string, render: () => unknown, options?: {
|
|
155
151
|
load?: ServerPageLoader;
|
|
156
152
|
pattern?: string;
|
package/dist/ssr.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { C as setLoaderGuard, S as setFrameLoaderRunner, _ as readBodyBounded, a as __ilhaServerAction, b as setFrameAuth, c as frameEnvelope, d as getFrameLoaderRunner, f as getLoaderGuard, g as json, h as isTrustedOrigin, i as MAX_BODY, l as getFrameAuth, m as isSafeFramePath, n as FrameError, o as authorizeFrameRequest, p as getServerIslandEntry, r as LOADER_ENDPOINT, s as forwardIdentityHeaders, t as FRAME_ENDPOINT, u as getFrameGuard, v as registerServerIsland, w as ssr, x as setFrameGuard, y as renderServerIsland } from "./ssr-
|
|
1
|
+
import { C as setLoaderGuard, S as setFrameLoaderRunner, _ as readBodyBounded, a as __ilhaServerAction, b as setFrameAuth, c as frameEnvelope, d as getFrameLoaderRunner, f as getLoaderGuard, g as json, h as isTrustedOrigin, i as MAX_BODY, l as getFrameAuth, m as isSafeFramePath, n as FrameError, o as authorizeFrameRequest, p as getServerIslandEntry, r as LOADER_ENDPOINT, s as forwardIdentityHeaders, t as FRAME_ENDPOINT, u as getFrameGuard, v as registerServerIsland, w as ssr, x as setFrameGuard, y as renderServerIsland } from "./ssr-CGcMUP1G.js";
|
|
2
2
|
|
|
3
3
|
export { FRAME_ENDPOINT, FrameError, LOADER_ENDPOINT, MAX_BODY, __ilhaServerAction, authorizeFrameRequest, ssr as default, forwardIdentityHeaders, frameEnvelope, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, getServerIslandEntry, isSafeFramePath, isTrustedOrigin, json, readBodyBounded, registerServerIsland, renderServerIsland, setFrameAuth, setFrameGuard, setFrameLoaderRunner, setLoaderGuard };
|
package/dist/vite.js
CHANGED
package/oxlint.cjs
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
const PASCAL_CASE = /^[A-Z][a-zA-Z0-9]*$/;
|
|
2
|
+
const PRIMITIVES = new Set(["state", "derived", "action", "effect", "onError"]);
|
|
3
|
+
const ACTION_STATUS = new Set(["pending", "data", "error"]);
|
|
4
|
+
const CONDITIONAL = new Set([
|
|
5
|
+
"IfStatement",
|
|
6
|
+
"ForStatement",
|
|
7
|
+
"ForInStatement",
|
|
8
|
+
"ForOfStatement",
|
|
9
|
+
"WhileStatement",
|
|
10
|
+
"DoWhileStatement",
|
|
11
|
+
"SwitchStatement",
|
|
12
|
+
"ConditionalExpression",
|
|
13
|
+
"LogicalExpression",
|
|
14
|
+
]);
|
|
15
|
+
const FN_TYPES = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
16
|
+
|
|
17
|
+
function bindings() {
|
|
18
|
+
return { ilha: new Set(["ilha"]), prim: new Map() };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function takeImport(b, node) {
|
|
22
|
+
if (node.source.value !== "ilha") return;
|
|
23
|
+
for (const s of node.specifiers) {
|
|
24
|
+
if (s.type !== "ImportSpecifier") continue;
|
|
25
|
+
const imported = s.imported.name;
|
|
26
|
+
const local = s.local.name;
|
|
27
|
+
if (imported === "ilha") {
|
|
28
|
+
b.ilha.add(local);
|
|
29
|
+
}
|
|
30
|
+
if (PRIMITIVES.has(imported)) b.prim.set(local, imported);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isIlhaCall(node, b) {
|
|
35
|
+
return (
|
|
36
|
+
node.type === "CallExpression" &&
|
|
37
|
+
node.callee.type === "Identifier" &&
|
|
38
|
+
b.ilha.has(node.callee.name)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function primitiveName(node, b) {
|
|
43
|
+
if (node.type !== "CallExpression") return null;
|
|
44
|
+
const c = node.callee;
|
|
45
|
+
if (c.type === "Identifier") return b.prim.get(c.name) ?? null;
|
|
46
|
+
const effectLocal = [...b.prim].find(([, n]) => n === "effect")?.[0];
|
|
47
|
+
if (
|
|
48
|
+
effectLocal &&
|
|
49
|
+
c.type === "MemberExpression" &&
|
|
50
|
+
!c.computed &&
|
|
51
|
+
c.object.type === "Identifier" &&
|
|
52
|
+
c.object.name === effectLocal &&
|
|
53
|
+
c.property.type === "Identifier" &&
|
|
54
|
+
c.property.name === "once"
|
|
55
|
+
) {
|
|
56
|
+
return "effect.once";
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fnName(node, parent) {
|
|
62
|
+
if (node.id && node.id.type === "Identifier") return node.id.name;
|
|
63
|
+
if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier") {
|
|
64
|
+
return parent.id.name;
|
|
65
|
+
}
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isComponentFn(node, parent, ancestors, b) {
|
|
70
|
+
const name = fnName(node, parent);
|
|
71
|
+
if (PASCAL_CASE.test(name)) return true;
|
|
72
|
+
if (parent?.type === "CallExpression" && isIlhaCall(parent, b)) return true;
|
|
73
|
+
const grand = ancestors?.[ancestors.length - 2];
|
|
74
|
+
return !!(grand && isIlhaCall(grand, b));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ancestorsOf(context, node) {
|
|
78
|
+
const sc = context.sourceCode;
|
|
79
|
+
if (sc && typeof sc.getAncestors === "function") return sc.getAncestors(node);
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parentOf(context, node) {
|
|
84
|
+
const a = ancestorsOf(context, node);
|
|
85
|
+
return a[a.length - 1];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toPascalCase(name) {
|
|
89
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function calleeName(node) {
|
|
93
|
+
return node.type === "Identifier" ? node.name : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function collectIslands(b) {
|
|
97
|
+
const names = new Set();
|
|
98
|
+
return {
|
|
99
|
+
names,
|
|
100
|
+
VariableDeclarator(node) {
|
|
101
|
+
if (node.id.type === "Identifier" && node.init && isIlhaCall(node.init, b)) {
|
|
102
|
+
names.add(node.id.name);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isInsideComponent(context, node, b) {
|
|
109
|
+
const ancestors = ancestorsOf(context, node);
|
|
110
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
111
|
+
const a = ancestors[i];
|
|
112
|
+
if (!FN_TYPES.has(a.type)) continue;
|
|
113
|
+
const parent = i > 0 ? ancestors[i - 1] : null;
|
|
114
|
+
if (isComponentFn(a, parent, ancestors.slice(0, i), b)) return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function islandCallee(names, callee) {
|
|
120
|
+
if (callee.type === "Identifier" && names.has(callee.name)) return callee.name;
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const pascalCase = {
|
|
125
|
+
meta: {
|
|
126
|
+
type: "suggestion",
|
|
127
|
+
fixable: "code",
|
|
128
|
+
docs: { description: "Enforce PascalCase for ilha island variable names" },
|
|
129
|
+
messages: {
|
|
130
|
+
notPascalCase: 'Island variable "{{name}}" must be PascalCase (e.g. "{{suggested}}").',
|
|
131
|
+
},
|
|
132
|
+
schema: [],
|
|
133
|
+
},
|
|
134
|
+
create(context) {
|
|
135
|
+
const b = bindings();
|
|
136
|
+
return {
|
|
137
|
+
ImportDeclaration(node) {
|
|
138
|
+
takeImport(b, node);
|
|
139
|
+
},
|
|
140
|
+
VariableDeclarator(node) {
|
|
141
|
+
if (node.id.type === "Identifier" && node.init && isIlhaCall(node.init, b)) {
|
|
142
|
+
const name = node.id.name;
|
|
143
|
+
if (!PASCAL_CASE.test(name)) {
|
|
144
|
+
const suggested = toPascalCase(name);
|
|
145
|
+
context.report({
|
|
146
|
+
node: node.id,
|
|
147
|
+
messageId: "notPascalCase",
|
|
148
|
+
data: { name, suggested },
|
|
149
|
+
fix(fixer) {
|
|
150
|
+
return fixer.replaceText(node.id, suggested);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const noConditionalPrimitive = {
|
|
161
|
+
meta: {
|
|
162
|
+
type: "problem",
|
|
163
|
+
docs: { description: "Disallow primitives inside conditionals or loops" },
|
|
164
|
+
messages: {
|
|
165
|
+
conditional:
|
|
166
|
+
"Do not call {{name}}() inside a condition or loop. Put the branch inside the primitive.",
|
|
167
|
+
},
|
|
168
|
+
schema: [],
|
|
169
|
+
},
|
|
170
|
+
create(context) {
|
|
171
|
+
const b = bindings();
|
|
172
|
+
return {
|
|
173
|
+
ImportDeclaration(node) {
|
|
174
|
+
takeImport(b, node);
|
|
175
|
+
},
|
|
176
|
+
CallExpression(node) {
|
|
177
|
+
const name = primitiveName(node, b);
|
|
178
|
+
if (!name) return;
|
|
179
|
+
const ancestors = ancestorsOf(context, node);
|
|
180
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
181
|
+
const a = ancestors[i];
|
|
182
|
+
if (FN_TYPES.has(a.type)) return;
|
|
183
|
+
if (CONDITIONAL.has(a.type)) {
|
|
184
|
+
context.report({ node, messageId: "conditional", data: { name } });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const noPrimitiveOutsideIsland = {
|
|
194
|
+
meta: {
|
|
195
|
+
type: "problem",
|
|
196
|
+
docs: { description: "Only call primitives in an island render or PascalCase component" },
|
|
197
|
+
messages: {
|
|
198
|
+
outside:
|
|
199
|
+
"Call {{name}}() only in an ilha() render or a PascalCase component. Put setup in effect.once().",
|
|
200
|
+
},
|
|
201
|
+
schema: [],
|
|
202
|
+
},
|
|
203
|
+
create(context) {
|
|
204
|
+
const b = bindings();
|
|
205
|
+
return {
|
|
206
|
+
ImportDeclaration(node) {
|
|
207
|
+
takeImport(b, node);
|
|
208
|
+
},
|
|
209
|
+
CallExpression(node) {
|
|
210
|
+
const name = primitiveName(node, b);
|
|
211
|
+
if (!name) return;
|
|
212
|
+
const ancestors = ancestorsOf(context, node);
|
|
213
|
+
const fns = ancestors.filter((a) => FN_TYPES.has(a.type));
|
|
214
|
+
if (fns.length === 0) {
|
|
215
|
+
context.report({ node, messageId: "outside", data: { name } });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const enclosing = fns[fns.length - 1];
|
|
219
|
+
const idx = ancestors.indexOf(enclosing);
|
|
220
|
+
const parent = idx > 0 ? ancestors[idx - 1] : parentOf(context, enclosing);
|
|
221
|
+
if (isComponentFn(enclosing, parent, ancestors.slice(0, idx), b)) return;
|
|
222
|
+
context.report({ node, messageId: "outside", data: { name } });
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const preferPlainHandler = {
|
|
229
|
+
meta: {
|
|
230
|
+
type: "suggestion",
|
|
231
|
+
docs: { description: "Use a plain function unless action status or cancellation is used" },
|
|
232
|
+
messages: {
|
|
233
|
+
unused:
|
|
234
|
+
"action() is unused as a status object. Use a plain function unless you read .pending, .data, or .error.",
|
|
235
|
+
},
|
|
236
|
+
schema: [],
|
|
237
|
+
},
|
|
238
|
+
create(context) {
|
|
239
|
+
const b = bindings();
|
|
240
|
+
const found = new Map();
|
|
241
|
+
return {
|
|
242
|
+
ImportDeclaration(node) {
|
|
243
|
+
takeImport(b, node);
|
|
244
|
+
},
|
|
245
|
+
VariableDeclarator(node) {
|
|
246
|
+
if (node.id.type !== "Identifier" || !node.init) return;
|
|
247
|
+
if (primitiveName(node.init, b) !== "action") return;
|
|
248
|
+
found.set(node.id.name, { node: node.init, used: false });
|
|
249
|
+
},
|
|
250
|
+
MemberExpression(node) {
|
|
251
|
+
if (node.computed || node.object.type !== "Identifier") return;
|
|
252
|
+
if (node.property.type !== "Identifier" || !ACTION_STATUS.has(node.property.name)) return;
|
|
253
|
+
const rec = found.get(node.object.name);
|
|
254
|
+
if (rec) rec.used = true;
|
|
255
|
+
},
|
|
256
|
+
"Program:exit"() {
|
|
257
|
+
for (const rec of found.values()) {
|
|
258
|
+
if (!rec.used) context.report({ node: rec.node, messageId: "unused" });
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const preferLowercaseEvents = {
|
|
266
|
+
meta: {
|
|
267
|
+
type: "problem",
|
|
268
|
+
docs: { description: "Use lowercase DOM event props (onclick, not onClick)" },
|
|
269
|
+
messages: {
|
|
270
|
+
camel: 'Use lowercase event "{{suggested}}" instead of "{{name}}".',
|
|
271
|
+
},
|
|
272
|
+
schema: [],
|
|
273
|
+
},
|
|
274
|
+
create(context) {
|
|
275
|
+
function checkName(node, name) {
|
|
276
|
+
if (/^on[A-Z]/.test(name)) {
|
|
277
|
+
context.report({
|
|
278
|
+
node,
|
|
279
|
+
messageId: "camel",
|
|
280
|
+
data: { name, suggested: "on" + name.slice(2).toLowerCase() },
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
JSXAttribute(node) {
|
|
286
|
+
const parent = parentOf(context, node);
|
|
287
|
+
const tag = parent && parent.name;
|
|
288
|
+
if (tag && tag.type === "JSXIdentifier" && PASCAL_CASE.test(tag.name)) return;
|
|
289
|
+
if (node.name.type === "JSXIdentifier") checkName(node.name, node.name.name);
|
|
290
|
+
if (node.name.type === "JSXNamespacedName") {
|
|
291
|
+
checkName(node.name.namespace, node.name.namespace.name);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
TaggedTemplateExpression(node) {
|
|
295
|
+
if (node.tag.type !== "Identifier" || node.tag.name !== "html") return;
|
|
296
|
+
for (const q of node.quasi.quasis) {
|
|
297
|
+
const text = q.value.cooked ?? q.value.raw;
|
|
298
|
+
const re = /\bon[A-Z][A-Za-z]*/g;
|
|
299
|
+
let m;
|
|
300
|
+
while ((m = re.exec(text))) checkName(node, m[0]);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const noDirectIslandCall = {
|
|
308
|
+
meta: {
|
|
309
|
+
type: "problem",
|
|
310
|
+
docs: { description: "Only call islands as children inside another island render" },
|
|
311
|
+
messages: {
|
|
312
|
+
direct:
|
|
313
|
+
"Do not call {{name}}() here. Nest it inside another island, or use {{name}}.toString / toStringAsync / hydratable.",
|
|
314
|
+
},
|
|
315
|
+
schema: [],
|
|
316
|
+
},
|
|
317
|
+
create(context) {
|
|
318
|
+
const b = bindings();
|
|
319
|
+
const { names, VariableDeclarator } = collectIslands(b);
|
|
320
|
+
return {
|
|
321
|
+
ImportDeclaration(node) {
|
|
322
|
+
takeImport(b, node);
|
|
323
|
+
},
|
|
324
|
+
VariableDeclarator,
|
|
325
|
+
CallExpression(node) {
|
|
326
|
+
const name = islandCallee(names, node.callee);
|
|
327
|
+
if (!name) return;
|
|
328
|
+
if (isInsideComponent(context, node, b)) return;
|
|
329
|
+
context.report({ node, messageId: "direct", data: { name } });
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const requireSsrApi = {
|
|
336
|
+
meta: {
|
|
337
|
+
type: "problem",
|
|
338
|
+
docs: { description: "Use Island.toString / toStringAsync / hydratable for SSR" },
|
|
339
|
+
messages: {
|
|
340
|
+
awaitCall:
|
|
341
|
+
"Do not await {{name}}(). Use await {{name}}.toStringAsync() or await {{name}}.hydratable().",
|
|
342
|
+
},
|
|
343
|
+
schema: [],
|
|
344
|
+
},
|
|
345
|
+
create(context) {
|
|
346
|
+
const b = bindings();
|
|
347
|
+
const { names, VariableDeclarator } = collectIslands(b);
|
|
348
|
+
return {
|
|
349
|
+
ImportDeclaration(node) {
|
|
350
|
+
takeImport(b, node);
|
|
351
|
+
},
|
|
352
|
+
VariableDeclarator,
|
|
353
|
+
AwaitExpression(node) {
|
|
354
|
+
const arg = node.argument;
|
|
355
|
+
if (arg.type !== "CallExpression") return;
|
|
356
|
+
const name = islandCallee(names, arg.callee);
|
|
357
|
+
if (name) context.report({ node, messageId: "awaitCall", data: { name } });
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const functionInState = {
|
|
364
|
+
meta: {
|
|
365
|
+
type: "problem",
|
|
366
|
+
docs: { description: "Do not pass a function value to state() or a state setter" },
|
|
367
|
+
messages: {
|
|
368
|
+
init: "state({{name}}) treats the function as a lazy initializer. Wrap it: state(() => {{name}}).",
|
|
369
|
+
set: "{{setter}}({{name}}) runs as an updater. Store a function with {{setter}}(() => {{name}}).",
|
|
370
|
+
},
|
|
371
|
+
schema: [],
|
|
372
|
+
},
|
|
373
|
+
create(context) {
|
|
374
|
+
const b = bindings();
|
|
375
|
+
const fns = new Set();
|
|
376
|
+
const states = new Set();
|
|
377
|
+
return {
|
|
378
|
+
ImportDeclaration(node) {
|
|
379
|
+
takeImport(b, node);
|
|
380
|
+
},
|
|
381
|
+
FunctionDeclaration(node) {
|
|
382
|
+
if (node.id) fns.add(node.id.name);
|
|
383
|
+
},
|
|
384
|
+
VariableDeclarator(node) {
|
|
385
|
+
if (node.id.type !== "Identifier") return;
|
|
386
|
+
const init = node.init;
|
|
387
|
+
if (
|
|
388
|
+
init &&
|
|
389
|
+
(init.type === "FunctionExpression" || init.type === "ArrowFunctionExpression")
|
|
390
|
+
) {
|
|
391
|
+
fns.add(node.id.name);
|
|
392
|
+
}
|
|
393
|
+
if (init && primitiveName(init, b) === "state") states.add(node.id.name);
|
|
394
|
+
},
|
|
395
|
+
CallExpression(node) {
|
|
396
|
+
if (node.arguments.length === 0) return;
|
|
397
|
+
const arg = node.arguments[0];
|
|
398
|
+
if (arg.type !== "Identifier" || !fns.has(arg.name)) return;
|
|
399
|
+
if (primitiveName(node, b) === "state") {
|
|
400
|
+
context.report({ node: arg, messageId: "init", data: { name: arg.name } });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const setter = calleeName(node.callee);
|
|
404
|
+
if (setter && states.has(setter)) {
|
|
405
|
+
context.report({
|
|
406
|
+
node: arg,
|
|
407
|
+
messageId: "set",
|
|
408
|
+
data: { setter, name: arg.name },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
module.exports = {
|
|
417
|
+
meta: { name: "oxlint-plugin-ilha" },
|
|
418
|
+
rules: {
|
|
419
|
+
"pascal-case": pascalCase,
|
|
420
|
+
"no-conditional-primitive": noConditionalPrimitive,
|
|
421
|
+
"no-primitive-outside-island": noPrimitiveOutsideIsland,
|
|
422
|
+
"prefer-plain-handler": preferPlainHandler,
|
|
423
|
+
"prefer-lowercase-events": preferLowercaseEvents,
|
|
424
|
+
"no-direct-island-call": noDirectIslandCall,
|
|
425
|
+
"require-ssr-api": requireSsrApi,
|
|
426
|
+
"function-in-state": functionInState,
|
|
427
|
+
},
|
|
428
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ilha/router",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "A tiny SPA router for Ilha",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontend",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"url": "git+https://github.com/ilhajs/ilha.git"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
|
-
"dist"
|
|
29
|
+
"dist",
|
|
30
|
+
"oxlint.cjs"
|
|
30
31
|
],
|
|
31
32
|
"type": "module",
|
|
32
33
|
"sideEffects": false,
|
|
@@ -40,6 +41,10 @@
|
|
|
40
41
|
"types": "./dist/index.d.ts",
|
|
41
42
|
"import": "./dist/index.js"
|
|
42
43
|
},
|
|
44
|
+
"./server": {
|
|
45
|
+
"types": "./dist/server.d.ts",
|
|
46
|
+
"import": "./dist/server.js"
|
|
47
|
+
},
|
|
43
48
|
"./server-island": {
|
|
44
49
|
"types": "./dist/server-island.d.ts",
|
|
45
50
|
"import": "./dist/server-island.js"
|
|
@@ -55,7 +60,8 @@
|
|
|
55
60
|
"./rsbuild": {
|
|
56
61
|
"types": "./dist/rsbuild.d.ts",
|
|
57
62
|
"import": "./dist/rsbuild.js"
|
|
58
|
-
}
|
|
63
|
+
},
|
|
64
|
+
"./oxlint": "./oxlint.cjs"
|
|
59
65
|
},
|
|
60
66
|
"publishConfig": {
|
|
61
67
|
"access": "public"
|
|
@@ -69,10 +75,10 @@
|
|
|
69
75
|
"unplugin": "3.3.0"
|
|
70
76
|
},
|
|
71
77
|
"devDependencies": {
|
|
72
|
-
"ilha": "0.12.
|
|
78
|
+
"ilha": "0.12.1",
|
|
73
79
|
"vite": "^8.2.2"
|
|
74
80
|
},
|
|
75
81
|
"peerDependencies": {
|
|
76
|
-
"ilha": ">=0.12.
|
|
82
|
+
"ilha": ">=0.12.1"
|
|
77
83
|
}
|
|
78
84
|
}
|