@akanjs/devkit 3.0.0-alpha.6 → 3.0.0-alpha.7

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,35 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // Every prototype method of a `store(...)` class is registered as an action
5
+ // (StoreRegistry.register) and reaches callers only as `st.do.<action>`, which re-wraps it
6
+ // (StoreInstance.#extendAccessors) and is typed void by `VoidActions` in
7
+ // pkgs/akanjs/store/types.ts. A returned value is therefore unreachable from every call
8
+ // site — it reads like a contract while being dead code. Write the result into state with
9
+ // `this.set({ ... })` instead.
10
+ //
11
+ // Deliberately not flagged, because none of these are actions: a bare `return;` guard, a
12
+ // `return` belonging to a nested callback, a getter, a `static` helper, and a class-property
13
+ // arrow (an instance field, so `register` never sees it).
14
+ `return $value;` as $return where {
15
+ not $value <: r"",
16
+ $return <: within JsClassDeclaration() as $storeClass where {
17
+ $storeClass <: contains JsExtendsClause() as $extends where {
18
+ $extends <: r"extends\s+store\([\s\S]*"
19
+ }
20
+ },
21
+ $return <: within JsMethodClassMember() as $action where {
22
+ not $action <: r"static[\s\S]*"
23
+ },
24
+ not $return <: within JsArrowFunctionExpression(),
25
+ not $return <: within JsFunctionExpression(),
26
+ not $return <: within JsFunctionDeclaration(),
27
+ not $return <: within JsMethodObjectMember(),
28
+ not $return <: within JsGetterObjectMember(),
29
+ not $return <: within JsSetterObjectMember(),
30
+ register_diagnostic(
31
+ span = $return,
32
+ message = "Store actions dispatch as void — st.do.<action>() never exposes this value. Write the result into state with this.set({ ... }) and drop the returned value; a bare 'return;' guard clause is fine.",
33
+ severity = "error"
34
+ )
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.0.0-alpha.6",
3
+ "version": "3.0.0-alpha.7",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -44,7 +44,7 @@
44
44
  "@langchain/openai": "^1.4.6",
45
45
  "@tailwindcss/node": "^4.3.0",
46
46
  "@trapezedev/project": "^7.1.4",
47
- "akanjs": "3.0.0-alpha.6",
47
+ "akanjs": "3.0.0-alpha.7",
48
48
  "chalk": "^5.6.2",
49
49
  "commander": "^14.0.3",
50
50
  "dayjs": "^1.11.20",
package/qualityScanner.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readdir, readFile, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
4
5
  import ignore from "ignore";
5
6
  import ts from "typescript";
6
7
  import { AbstractDoc } from "./abstractDoc";
@@ -100,24 +101,6 @@ const CONVENTION_SUFFIXES = [
100
101
  ".store.ts",
101
102
  ] as const;
102
103
 
103
- // Non-PascalCase exports the framework recognizes on page/layout route modules (see PageModule/LayoutModule
104
- // in pkgs/akanjs/client/csrTypes.ts). PascalCase route exports (Loading, NotFound, Error) pass the component
105
- // check, and the `default` export is handled separately.
106
- const PAGE_RESERVED_EXPORTS = new Set([
107
- "pageConfig",
108
- "head",
109
- "metadata",
110
- "generateHead",
111
- "generateMetadata",
112
- "fonts",
113
- "manifest",
114
- "theme",
115
- "reconnect",
116
- "wsConnect",
117
- "layoutStyle",
118
- "gaTrackingId",
119
- ]);
120
-
121
104
  // How to remediate each rule, keyed by rule id. Surfaced as a `fix:` line per warning (text + JSON output)
122
105
  // so the scan result tells the reader what to do, not just what is wrong.
123
106
  const RULE_FIXES: Record<string, string> = {
@@ -669,7 +652,7 @@ function isRestrictedInternalKind(kind: ComponentFileDeclaration["kind"]) {
669
652
 
670
653
  function isAllowedComponentExport(declaration: ComponentFileDeclaration, isPage: boolean) {
671
654
  if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name)) return true;
672
- return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
655
+ return isPage && RESERVED_ROUTE_CONFIG_EXPORTS.has(declaration.name);
673
656
  }
674
657
 
675
658
  function isPascalCaseName(name: string) {
@@ -0,0 +1,41 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { RouteSourceValidator } from "./routeSourceValidator";
4
+
5
+ const validate = (source: string, kind: "page" | "layout", rootLayout = false) =>
6
+ RouteSourceValidator.validateRouteSourceExports(source, `page/_${kind}.tsx`, kind, { rootLayout });
7
+
8
+ describe("RouteSourceValidator", () => {
9
+ test("accepts every root-layout config export the route tree honors", () => {
10
+ const source = [
11
+ "export default function Layout() { return null; }",
12
+ "export const fonts = [];",
13
+ "export const manifest = {};",
14
+ 'export const theme = "dark";',
15
+ "export const reconnect = true;",
16
+ "export const wsConnect = true;",
17
+ "export const layoutStyle = {};",
18
+ 'export const gaTrackingId = "G-1";',
19
+ ].join("\n");
20
+
21
+ expect(() => validate(source, "layout", true)).not.toThrow();
22
+ });
23
+
24
+ test("rejects root-layout-only exports on a nested layout and on a page", () => {
25
+ const source = ["export default function Layout() { return null; }", "export const wsConnect = true;"].join("\n");
26
+
27
+ expect(() => validate(source, "layout")).toThrow('unsupported export "wsConnect"');
28
+ expect(() => validate(source, "page")).toThrow('unsupported export "wsConnect"');
29
+ });
30
+
31
+ test("reads devOnly off pageConfig without evaluating the module", () => {
32
+ const source = [
33
+ "export default function Page() { return null; }",
34
+ "export const pageConfig = { devOnly: true };",
35
+ ].join("\n");
36
+
37
+ expect(RouteSourceValidator.validateRouteSourceExports(source, "page/_index.tsx", "page")).toEqual({
38
+ devOnly: true,
39
+ });
40
+ });
41
+ });
@@ -1,3 +1,4 @@
1
+ import { getRouteExports } from "akanjs/common";
1
2
  import ts from "typescript";
2
3
 
3
4
  /** What the build needs out of a route module without evaluating it. */
@@ -16,44 +17,6 @@ export interface RouteSourceInfo {
16
17
  * validate a route source stay lean.
17
18
  */
18
19
  export class RouteSourceValidator {
19
- static readonly #pageExports = new Set([
20
- "default",
21
- "pageConfig",
22
- "head",
23
- "metadata",
24
- "generateHead",
25
- "generateMetadata",
26
- "Loading",
27
- ]);
28
- static readonly #rootLayoutExports = new Set([
29
- "default",
30
- "pageConfig",
31
- "head",
32
- "metadata",
33
- "generateHead",
34
- "generateMetadata",
35
- "fonts",
36
- "manifest",
37
- "theme",
38
- "reconnect",
39
- "layoutStyle",
40
- "gaTrackingId",
41
- "Loading",
42
- "NotFound",
43
- "Error",
44
- ]);
45
- static readonly #layoutExports = new Set([
46
- "default",
47
- "pageConfig",
48
- "head",
49
- "metadata",
50
- "generateHead",
51
- "generateMetadata",
52
- "Loading",
53
- "NotFound",
54
- "Error",
55
- ]);
56
-
57
20
  static validateRouteSourceExports(
58
21
  source: string,
59
22
  filePath: string,
@@ -61,12 +24,7 @@ export class RouteSourceValidator {
61
24
  options: { rootLayout?: boolean } = {},
62
25
  ): RouteSourceInfo {
63
26
  const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
64
- const allowed =
65
- kind === "page"
66
- ? RouteSourceValidator.#pageExports
67
- : options.rootLayout
68
- ? RouteSourceValidator.#rootLayoutExports
69
- : RouteSourceValidator.#layoutExports;
27
+ const allowed = getRouteExports(kind, { rootLayout: options.rootLayout });
70
28
  const exported = new Set<string>();
71
29
  const assertExport = (name: string) => {
72
30
  if (!allowed.has(name)) {