@akanjs/devkit 2.4.2-rc.3 → 2.4.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/artifact/implicitRootLayout.ts +2 -2
- package/executors.test.ts +58 -0
- package/executors.ts +3 -1
- package/package.json +2 -2
- package/routeSourceValidator.test.ts +30 -0
- package/routeSourceValidator.ts +1 -0
|
@@ -55,7 +55,7 @@ interface RootBoundary {
|
|
|
55
55
|
segments: string[];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function getRootBoundarySegments(key: string): string[] | null {
|
|
58
|
+
export function getRootBoundarySegments(key: string): string[] | null {
|
|
59
59
|
const match = LAYOUT_KEY_RE.exec(key);
|
|
60
60
|
if (!match) return null;
|
|
61
61
|
const prefix = match[1]?.replace(/\/$/, "");
|
|
@@ -76,7 +76,7 @@ function implicitDictionaryMacroAbsPath(appCwdPath: string): string {
|
|
|
76
76
|
return path.join(path.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function isRootBoundarySegments(segments: string[], basePaths: Iterable<string>): boolean {
|
|
79
|
+
export function isRootBoundarySegments(segments: string[], basePaths: Iterable<string>): boolean {
|
|
80
80
|
const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
|
|
81
81
|
if (firstVisibleIndex === -1) return segments.length <= 1;
|
|
82
82
|
if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment))) return false;
|
package/executors.test.ts
CHANGED
|
@@ -620,6 +620,64 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
620
620
|
});
|
|
621
621
|
});
|
|
622
622
|
|
|
623
|
+
describe("root layout source validation during page key discovery", () => {
|
|
624
|
+
const makeRouteValidationApp = async (appName: string, config: string, files: Record<string, string>) => {
|
|
625
|
+
const root = await makeTempRoot();
|
|
626
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
627
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
628
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
629
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
630
|
+
await mkdir(path.join(root, "apps", appName), { recursive: true });
|
|
631
|
+
await writeFile(path.join(root, `apps/${appName}/akan.config.ts`), config);
|
|
632
|
+
for (const [file, source] of Object.entries(files)) {
|
|
633
|
+
const filePath = path.join(root, `apps/${appName}/page`, file);
|
|
634
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
635
|
+
await writeFile(filePath, source);
|
|
636
|
+
}
|
|
637
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
638
|
+
return AppExecutor.from(workspace, appName);
|
|
639
|
+
};
|
|
640
|
+
const wsConnectLayout = [
|
|
641
|
+
"export const wsConnect = false;",
|
|
642
|
+
"export default function Layout({ children }) { return children; }",
|
|
643
|
+
"",
|
|
644
|
+
].join("\n");
|
|
645
|
+
|
|
646
|
+
test("accepts wsConnect on a configured base-path root layout", async () => {
|
|
647
|
+
process.env.AKAN_PUBLIC_BASE_PATHS = "web,admin";
|
|
648
|
+
const app = await makeRouteValidationApp(
|
|
649
|
+
"base-path-root-ws",
|
|
650
|
+
[
|
|
651
|
+
"export default {",
|
|
652
|
+
' routes: [{ basePath: "web", domains: {} }, { basePath: "admin", domains: {} }],',
|
|
653
|
+
"};",
|
|
654
|
+
"",
|
|
655
|
+
].join("\n"),
|
|
656
|
+
{ "admin/_layout.tsx": wsConnectLayout },
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
await expect(app.getPageKeys({ refresh: true })).resolves.toEqual(["./admin/_layout.tsx"]);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("rejects wsConnect on a nested layout", async () => {
|
|
663
|
+
const app = await makeRouteValidationApp(
|
|
664
|
+
"nested-layout-ws",
|
|
665
|
+
'export default { routes: [{ basePath: "admin", domains: {} }] };\n',
|
|
666
|
+
{ "admin/users/_layout.tsx": wsConnectLayout },
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(/unsupported export "wsConnect"/);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("accepts wsConnect on a grouped root layout", async () => {
|
|
673
|
+
const app = await makeRouteValidationApp("grouped-root-ws", "export default {};\n", {
|
|
674
|
+
"(docs)/_layout.tsx": wsConnectLayout,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
await expect(app.getPageKeys({ refresh: true })).resolves.toEqual(["./(docs)/_layout.tsx"]);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
623
681
|
test("accepts metadata route exports during page key discovery", async () => {
|
|
624
682
|
const root = await makeTempRoot();
|
|
625
683
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
package/executors.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { $ } from "bun";
|
|
33
33
|
import chalk from "chalk";
|
|
34
34
|
import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from "./akanConfig";
|
|
35
|
+
import { getRootBoundarySegments, isRootBoundarySegments } from "./artifact/implicitRootLayout";
|
|
35
36
|
import { FileSys } from "./fileSys";
|
|
36
37
|
import { getDirname } from "./getDirname";
|
|
37
38
|
import { Linter } from "./linter";
|
|
@@ -1425,7 +1426,8 @@ export class AppExecutor extends SysExecutor {
|
|
|
1425
1426
|
);
|
|
1426
1427
|
}
|
|
1427
1428
|
if (!owner) owners.set(routeId, { absPath, fromLib });
|
|
1428
|
-
const
|
|
1429
|
+
const layoutSegments = getRootBoundarySegments(key);
|
|
1430
|
+
const isRootLayout = layoutSegments !== null && isRootBoundarySegments(layoutSegments, akanConfig.basePaths);
|
|
1429
1431
|
const routeSource = await Bun.file(absPath).text();
|
|
1430
1432
|
const validator = await AppExecutor.#getRouteSourceValidator();
|
|
1431
1433
|
if (parsed.kind === "overrides") validator.validateOverridesSourceExports(routeSource, absPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.4.2
|
|
3
|
+
"version": "2.4.2",
|
|
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": "2.4.2
|
|
47
|
+
"akanjs": "2.4.2",
|
|
48
48
|
"chalk": "^5.6.2",
|
|
49
49
|
"commander": "^14.0.3",
|
|
50
50
|
"daisyui": "5.5.23",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { RouteSourceValidator } from "./routeSourceValidator";
|
|
4
|
+
|
|
5
|
+
const source = `export const wsConnect = false;
|
|
6
|
+
export default function Layout() {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
describe("RouteSourceValidator root-layout exports", () => {
|
|
12
|
+
// The generator (implicitRootLayout), the runtime routeTreeBuilder, and the docs all
|
|
13
|
+
// support wsConnect on a root layout, but this validator forgot it, so declaring the
|
|
14
|
+
// documented switch failed typecheck/lint/build with "unsupported export".
|
|
15
|
+
test("accepts wsConnect on a root layout", () => {
|
|
16
|
+
expect(() =>
|
|
17
|
+
RouteSourceValidator.validateRouteSourceExports(source, "/app/page/admin/_layout.tsx", "layout", {
|
|
18
|
+
rootLayout: true,
|
|
19
|
+
}),
|
|
20
|
+
).not.toThrow();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("still rejects wsConnect on a nested layout", () => {
|
|
24
|
+
expect(() =>
|
|
25
|
+
RouteSourceValidator.validateRouteSourceExports(source, "/app/page/admin/users/_layout.tsx", "layout", {
|
|
26
|
+
rootLayout: false,
|
|
27
|
+
}),
|
|
28
|
+
).toThrow(/unsupported export "wsConnect"/);
|
|
29
|
+
});
|
|
30
|
+
});
|