@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 CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # @affordances/lint
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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)
@@ -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-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }