@affordances/lint 0.0.0-stage → 0.0.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.
- package/README.md +18 -2
- package/dist/index.d.ts +12 -0
- package/dist/index.js +93 -0
- package/package.json +36 -4
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @affordances/lint
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two lint rules for Affordances apps, for ESLint and oxlint's JS plugins:
|
|
4
|
+
|
|
5
|
+
- `affordances/require-affordance` warns when a control calls a server action (`action`, `formAction`, `onClick`, or `onSubmit` using an import from an `actions` module) outside an `<Affordance>`.
|
|
6
|
+
- `affordances/static-action-name` errors when `defineAction` has no `name`, or a name that isn't a string literal, which codegen can't read.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
vlt install -D @affordances/lint
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
// eslint.config.js
|
|
14
|
+
import affordances from '@affordances/lint'
|
|
15
|
+
|
|
16
|
+
export default [affordances.configs.recommended]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Docs: [https://github.com/vltpkg/affordances/blob/main/apps/docs/content/guides/lint.mdx](https://github.com/vltpkg/affordances/blob/main/apps/docs/content/guides/lint.mdx)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Rule } from "eslint";
|
|
2
|
+
declare const plugin: {
|
|
3
|
+
meta: {
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
rules: {
|
|
7
|
+
'require-affordance': Rule.RuleModule;
|
|
8
|
+
'static-action-name': Rule.RuleModule;
|
|
9
|
+
};
|
|
10
|
+
configs: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
export { plugin as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
const isActionsModule = (source) => /(^|\/)actions(\.[jt]sx?)?$/.test(source);
|
|
3
|
+
const jsxName = (node) => node.name.type === "JSXIdentifier" ? node.name.name : void 0;
|
|
4
|
+
const referenced = (expression) => {
|
|
5
|
+
if (!expression) return [];
|
|
6
|
+
if (expression.type === "Identifier") return [expression.name];
|
|
7
|
+
if (expression.type === "CallExpression") {
|
|
8
|
+
const callee = expression.callee;
|
|
9
|
+
if (callee.type === "MemberExpression") return referenced(callee.object);
|
|
10
|
+
return referenced(callee);
|
|
11
|
+
}
|
|
12
|
+
if (expression.type === "ArrowFunctionExpression" || expression.type === "FunctionExpression") {
|
|
13
|
+
const body = expression.body;
|
|
14
|
+
if (body.type !== "BlockStatement") return referenced(body);
|
|
15
|
+
return body.body.flatMap((statement) => statement.type === "ExpressionStatement" ? referenced(statement.expression) : statement.type === "ReturnStatement" ? referenced(statement.argument) : []);
|
|
16
|
+
}
|
|
17
|
+
if (expression.type === "AwaitExpression") return referenced(expression.argument);
|
|
18
|
+
return [];
|
|
19
|
+
};
|
|
20
|
+
const plugin = {
|
|
21
|
+
meta: { name: "affordances" },
|
|
22
|
+
rules: {
|
|
23
|
+
"require-affordance": {
|
|
24
|
+
meta: {
|
|
25
|
+
type: "suggestion",
|
|
26
|
+
docs: { description: "Wrap controls that call a server action in <Affordance>, so agents can find them" },
|
|
27
|
+
schema: [],
|
|
28
|
+
messages: { missing: "{{name}} is a server action, but this control isn’t inside <Affordance>. Wrap it so agents and the dev overlay can find it." }
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
const actions = /* @__PURE__ */ new Set();
|
|
32
|
+
return {
|
|
33
|
+
ImportDeclaration(node) {
|
|
34
|
+
if (typeof node.source.value !== "string" || !isActionsModule(node.source.value)) return;
|
|
35
|
+
for (const spec of node.specifiers) if (spec.type === "ImportSpecifier") actions.add(spec.local.name);
|
|
36
|
+
},
|
|
37
|
+
JSXAttribute(attribute) {
|
|
38
|
+
const node = attribute;
|
|
39
|
+
const name = node.name.name;
|
|
40
|
+
if (name !== "action" && name !== "formAction" && name !== "onClick" && name !== "onSubmit") return;
|
|
41
|
+
const value = node.value;
|
|
42
|
+
if (value?.type !== "JSXExpressionContainer") return;
|
|
43
|
+
const used = referenced(value.expression).filter((id) => actions.has(id));
|
|
44
|
+
if (!used.length) return;
|
|
45
|
+
for (let parent = node.parent; parent; parent = parent.parent) if (parent.type === "JSXElement" && jsxName(parent.openingElement) === "Affordance") return;
|
|
46
|
+
context.report({
|
|
47
|
+
node: attribute,
|
|
48
|
+
messageId: "missing",
|
|
49
|
+
data: { name: used[0] }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"static-action-name": {
|
|
56
|
+
meta: {
|
|
57
|
+
type: "problem",
|
|
58
|
+
docs: { description: "defineAction needs a string literal name, so the manifest can be built statically" },
|
|
59
|
+
schema: [],
|
|
60
|
+
messages: {
|
|
61
|
+
literal: "defineAction’s `name` must be a string literal.",
|
|
62
|
+
missing: "defineAction needs a `name`."
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
create(context) {
|
|
66
|
+
return { CallExpression(node) {
|
|
67
|
+
const callee = node.callee;
|
|
68
|
+
if ((callee.type === "Identifier" ? callee.name : callee.type === "MemberExpression" && callee.property.type === "Identifier" ? callee.property.name : void 0) !== "defineAction") return;
|
|
69
|
+
const [config] = node.arguments;
|
|
70
|
+
if (config?.type !== "ObjectExpression") return;
|
|
71
|
+
const name = config.properties.find((p) => p.type === "Property" && p.key.type === "Identifier" && p.key.name === "name");
|
|
72
|
+
if (!name || name.type !== "Property") return context.report({
|
|
73
|
+
node,
|
|
74
|
+
messageId: "missing"
|
|
75
|
+
});
|
|
76
|
+
if (name.value.type !== "Literal" || typeof name.value.value !== "string") context.report({
|
|
77
|
+
node: name.value,
|
|
78
|
+
messageId: "literal"
|
|
79
|
+
});
|
|
80
|
+
} };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
configs: {}
|
|
85
|
+
};
|
|
86
|
+
plugin.configs.recommended = {
|
|
87
|
+
plugins: { affordances: plugin },
|
|
88
|
+
rules: {
|
|
89
|
+
"affordances/require-affordance": "warn",
|
|
90
|
+
"affordances/static-action-name": "error"
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
export { plugin as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@affordances/lint",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Affordances lint rules for ESLint and oxlint: require-affordance, static-action-name",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/vltpkg/affordances.git",
|
|
9
|
+
"directory": "packages/lint"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsdown",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "vitest run --passWithNoTests"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"eslint": "^10"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"eslint": ">=9"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"eslint": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|