@akanjs/devkit 3.0.1 → 3.0.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +15 -2
  2. package/akanApp/akanApp.host.test.ts +62 -0
  3. package/akanApp/akanApp.host.ts +13 -2
  4. package/akanApp/devHostPolicy.test.ts +14 -0
  5. package/akanApp/devHostPolicy.ts +11 -0
  6. package/akanConfig/akanConfig.test.ts +4 -1
  7. package/akanConfig/akanConfig.ts +3 -1
  8. package/akanContext.ts +18 -1
  9. package/biome.base.json +8 -1
  10. package/commandDecorators/command.ts +3 -2
  11. package/commandDecorators/commandDecorators.test.ts +7 -0
  12. package/createTunnel.test.ts +51 -0
  13. package/createTunnel.ts +32 -14
  14. package/csrE2e/csrPageStack.e2e.test.ts +12 -3
  15. package/executors.test.ts +329 -4
  16. package/executors.ts +49 -47
  17. package/frontendBuild/csrArtifactBuilder.test.ts +34 -0
  18. package/frontendBuild/csrArtifactBuilder.ts +14 -8
  19. package/frontendBuild/csrDevModuleCompiler.ts +0 -1
  20. package/frontendBuild/csrDevResolver.ts +0 -2
  21. package/frontendBuild/devGeneratedIndexSync.test.ts +13 -1
  22. package/frontendBuild/devGeneratedIndexSync.ts +21 -4
  23. package/frontendBuild/frontendBuild.test.ts +1 -1
  24. package/jsonFormatter.test.ts +64 -0
  25. package/jsonFormatter.ts +78 -0
  26. package/lint/__fixtures__/README.md +12 -3
  27. package/lint/__fixtures__/no-double-brace-placeholder/bad.ts +22 -0
  28. package/lint/__fixtures__/no-double-brace-placeholder/fixture.json +1 -0
  29. package/lint/__fixtures__/no-double-brace-placeholder/good.ts +15 -0
  30. package/lint/__fixtures__/no-inline-color/bad.tsx +17 -0
  31. package/lint/__fixtures__/no-inline-color/good.tsx +13 -0
  32. package/lint/__fixtures__/no-throw-raw-error/bad.tsx +8 -0
  33. package/lint/__fixtures__/no-throw-raw-error/good.tsx +8 -1
  34. package/lint/__fixtures__/no-unguarded-endpoint/bad.ts +20 -0
  35. package/lint/__fixtures__/no-unguarded-endpoint/fixture.json +1 -0
  36. package/lint/__fixtures__/no-unguarded-endpoint/good.ts +28 -0
  37. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -7
  38. package/lint/gritRules.test.ts +23 -15
  39. package/lint/no-double-brace-placeholder.grit +15 -0
  40. package/lint/no-inline-color.grit +21 -5
  41. package/lint/no-throw-raw-error.grit +18 -33
  42. package/lint/no-unguarded-endpoint.grit +73 -0
  43. package/manifestDependencies.ts +65 -0
  44. package/mobile/desktopBin.test.ts +1 -1
  45. package/mobile/desktopServerStage.test.ts +7 -4
  46. package/mobile/nativeApp.test.ts +57 -8
  47. package/mobile/nativeApp.ts +27 -9
  48. package/mobile/nativeConfig.test.ts +26 -4
  49. package/mobile/nativeConfig.ts +19 -1
  50. package/package.json +2 -2
  51. package/platformTest/LinuxDockerTarget.ts +1 -1
  52. package/platformTest/PlatformTestTarget.ts +3 -1
  53. package/platformTest/WindowsSshTarget.ts +1 -1
  54. package/qualityScanner.test.ts +51 -0
  55. package/qualityScanner.ts +22 -1
  56. package/scanInfo.ts +14 -2
  57. package/testHelpers.ts +17 -0
  58. package/transforms/transforms.test.ts +13 -0
  59. package/tsconfig.json +4 -15
  60. package/workspaceLayout.test.ts +78 -1
  61. package/workspaceLayout.ts +19 -2
package/scanInfo.ts CHANGED
@@ -10,7 +10,14 @@ import type {
10
10
  } from "./akanConfig";
11
11
  import { AkanAppConfig } from "./akanConfig";
12
12
  import { AppExecutor, LibExecutor, PkgExecutor, WorkspaceExecutor } from "./executors";
13
- import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles, rootEntryHintOf } from "./workspaceLayout";
13
+ import {
14
+ barrelFacetDirs,
15
+ facetIndexShadowOf,
16
+ isAllowedLibFacetRootFile,
17
+ rootAllowedDirs,
18
+ rootAllowedFiles,
19
+ rootEntryHintOf,
20
+ } from "./workspaceLayout";
14
21
 
15
22
  const scalarFileTypes = ["constant", "dictionary", "document", "template", "unit", "util", "view", "zone"] as const;
16
23
  type ScalarFileType = (typeof scalarFileTypes)[number];
@@ -74,6 +81,11 @@ async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: {
74
81
  for (const dirname of dirs)
75
82
  if (!allowedRootDirs.has(dirname))
76
83
  addViolation(dirname, rootEntryHintOf(exec.type, dirname) ?? `unsupported ${exec.type} root folder`);
84
+ for (const facet of barrelFacetDirs)
85
+ for (const filename of await exec.readdir(facet)) {
86
+ const shadow = facetIndexShadowOf(`${facet}/${filename}`);
87
+ if (shadow) addViolation(path.join(facet, filename), shadow);
88
+ }
77
89
 
78
90
  //* A lib has no `getPageKeys`, so its own route files are validated here.
79
91
  if (exec.type === "lib")
@@ -275,7 +287,7 @@ class ScanInfo {
275
287
  }
276
288
  }
277
289
 
278
- const isAkanFrameworkDependency = (dep: string) => dep === "akanjs" || dep.startsWith("akanjs/");
290
+ export const isAkanFrameworkDependency = (dep: string) => dep === "akanjs" || dep.startsWith("akanjs/");
279
291
  export class AppInfo extends ScanInfo {
280
292
  readonly type = "app";
281
293
  readonly exec: AppExecutor;
package/testHelpers.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach } from "bun:test";
2
+ import { existsSync } from "node:fs";
2
3
  import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
@@ -122,6 +123,22 @@ export const writeJson = async (filePath: string, value: object) => {
122
123
  await writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
123
124
  };
124
125
 
126
+ const repoRoot = path.resolve(import.meta.dir, "../../..");
127
+ const biomeBin = path.join(repoRoot, "node_modules", ".bin", process.platform === "win32" ? "biome.exe" : "biome");
128
+ export const hasBiome = existsSync(biomeBin);
129
+
130
+ //? Through stdin under a repo-relative name, so the repo's own Biome config and overrides judge content kept elsewhere.
131
+ export const formatWithBiome = async (content: string, repoPath: string) => {
132
+ const proc = Bun.spawn([biomeBin, "check", "--write", `--stdin-file-path=${repoPath}`], {
133
+ cwd: repoRoot,
134
+ stdin: new Blob([content]),
135
+ stdout: "pipe",
136
+ stderr: "pipe",
137
+ });
138
+ const [output] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
139
+ return output;
140
+ };
141
+
125
142
  const tsconfigJson = { compilerOptions: { target: "ESNext", paths: {} } };
126
143
 
127
144
  export const createTempApp = async (appName = "demo", parentDir?: string) => {
@@ -230,6 +230,19 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
230
230
  expect(secondMap?.get("b2")).toEqual({ subpath: "second/b2", originalName: "b2" });
231
231
  });
232
232
 
233
+ test("an empty facet barrel maps no name, so an import from it is left as written", async () => {
234
+ const root = await makeTempRoot();
235
+ const pkgDir = path.join(root, "common");
236
+ await write(path.join(pkgDir, "index.ts"), "export {};\n");
237
+ const analyzer = new BarrelAnalyzer({
238
+ resolvePackage: async () => ({ pkgName: "@libs/util/common", entryFile: path.join(pkgDir, "index.ts"), pkgDir }),
239
+ });
240
+
241
+ expect(await analyzer.analyze("@libs/util/common")).toEqual(new Map());
242
+ const source = 'import { pad } from "@libs/util/common";\n';
243
+ expect(await rewriteBarrelImports(source, ["@libs/util/common"], analyzer)).toBeNull();
244
+ });
245
+
233
246
  test("rewrites flattenable named imports and preserves default, type, and unknown imports", async () => {
234
247
  const analyzer = fakeAnalyzer([
235
248
  ["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
package/tsconfig.json CHANGED
@@ -1,17 +1,12 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "lib": [
4
- "ESNext",
5
- "DOM"
6
- ],
3
+ "lib": ["ESNext", "DOM"],
7
4
  "target": "ESNext",
8
5
  "module": "esnext",
9
6
  "moduleDetection": "force",
10
7
  "jsx": "react-jsx",
11
8
  "allowJs": true,
12
- "types": [
13
- "bun"
14
- ],
9
+ "types": ["bun"],
15
10
  "moduleResolution": "bundler",
16
11
  "allowImportingTsExtensions": true,
17
12
  "verbatimModuleSyntax": true,
@@ -30,12 +25,6 @@
30
25
  },
31
26
  "extends": "../../../tsconfig.json",
32
27
  "files": [],
33
- "include": [
34
- "**/*.ts",
35
- "**/*.tsx",
36
- "../../akanjs/server/types/*.d.ts"
37
- ],
38
- "exclude": [
39
- "lint/__fixtures__"
40
- ]
28
+ "include": ["**/*.ts", "**/*.tsx", "../../akanjs/server/types/*.d.ts"],
29
+ "exclude": ["lint/__fixtures__"]
41
30
  }
@@ -1,7 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { readdir } from "node:fs/promises";
3
+ import path from "node:path";
2
4
  import {
3
5
  appRootAllowedDirs,
4
6
  appRootAllowedFiles,
7
+ facetIndexShadowOf,
5
8
  isAllowedLibFacetRootFile,
6
9
  isScannedRootEntry,
7
10
  libRootAllowedDirs,
@@ -34,7 +37,7 @@ describe("app root layout allowlist", () => {
34
37
  test("names what a Capacitor app kept in its root as a leftover, not an unknown entry", () => {
35
38
  for (const name of ["android", "ios", "mobile", "capacitor.config.ts", "capacitor.config.json"]) {
36
39
  expect(appRootAllowedFiles.has(name) || appRootAllowedDirs.has(name)).toBe(false);
37
- expect(rootEntryHintOf("app", name)).toContain(".akan/native/<target>");
40
+ expect(rootEntryHintOf("app", name)).toContain("dist/native/<app>/<target>");
38
41
  }
39
42
  expect(rootEntryHintOf("app", "base")).toBeNull();
40
43
  expect(rootEntryHintOf("lib", "ios")).toBeNull();
@@ -78,6 +81,28 @@ describe("lib root layout allowlist", () => {
78
81
  });
79
82
  });
80
83
 
84
+ describe("hand-written facet index", () => {
85
+ test("flags every index Bun resolves ahead of the generated barrel, at the root of each barrel facet", () => {
86
+ for (const facet of ["ui", "webkit", "srvkit", "common", "plugin"])
87
+ for (const filename of ["index.tsx", "index.jsx", "index.mts"])
88
+ expect(facetIndexShadowOf(`${facet}/${filename}`)).toContain(`shadows the generated ${facet}/index.ts`);
89
+ expect(facetIndexShadowOf("ui/index.tsx")).toContain("Delete it");
90
+ });
91
+
92
+ test("leaves the generated barrel, a folder namespace, and folders without a barrel alone", () => {
93
+ for (const relativePath of [
94
+ "ui/index.ts",
95
+ "ui/index.js",
96
+ "ui/Chat/index.tsx",
97
+ "ui/Chat/index_.tsx",
98
+ "page/index.tsx",
99
+ "lib/index.tsx",
100
+ "index.tsx",
101
+ ])
102
+ expect(facetIndexShadowOf(relativePath)).toBeNull();
103
+ });
104
+ });
105
+
81
106
  describe("lib facet root allowlist", () => {
82
107
  test("admits the generated support facets and a root signal test", () => {
83
108
  expect(isAllowedLibFacetRootFile("cnst.ts")).toBe(true);
@@ -91,3 +116,55 @@ describe("lib facet root allowlist", () => {
91
116
  expect(isAllowedLibFacetRootFile("user.test.ts")).toBe(false);
92
117
  });
93
118
  });
119
+
120
+ const templatesDir = path.join(import.meta.dir, "..", "cli", "templates");
121
+
122
+ type TsConfig = { references?: { path: string }[] };
123
+
124
+ const loadRootTemplate = async (dir: string): Promise<TsConfig> =>
125
+ JSON.parse(await Bun.file(path.join(templatesDir, dir, "tsconfig.json.template")).text());
126
+
127
+ const templateDirsWithRootTsconfig = async (): Promise<string[]> => {
128
+ const entries = await readdir(templatesDir, { withFileTypes: true });
129
+ const dirs: string[] = [];
130
+ for (const entry of entries) {
131
+ if (!entry.isDirectory()) continue;
132
+ if (await Bun.file(path.join(templatesDir, entry.name, "tsconfig.json.template")).exists()) dirs.push(entry.name);
133
+ }
134
+ return dirs.sort((a, b) => a.localeCompare(b));
135
+ };
136
+
137
+ describe("cli root tsconfig templates vs workspace scan allowlists", () => {
138
+ test("only the app and libRoot root templates may declare references", async () => {
139
+ const dirs = await templateDirsWithRootTsconfig();
140
+ for (const dir of ["app", "libRoot", "pkgRoot", "workspaceRoot"]) expect(dirs).toContain(dir);
141
+ for (const dir of dirs) {
142
+ const tsconfig = await loadRootTemplate(dir);
143
+ if (!tsconfig.references) continue;
144
+ expect(["app", "libRoot"], `${dir} template declares references`).toContain(dir);
145
+ }
146
+ });
147
+
148
+ test("app template references stay inside appRootAllowedFiles", async () => {
149
+ const tsconfig = await loadRootTemplate("app");
150
+ for (const reference of tsconfig.references ?? []) {
151
+ const filename = path.basename(reference.path);
152
+ expect(appRootAllowedFiles.has(filename), `app template references "${reference.path}"`).toBe(true);
153
+ }
154
+ });
155
+
156
+ test("libRoot template references stay inside libRootAllowedFiles", async () => {
157
+ const tsconfig = await loadRootTemplate("libRoot");
158
+ for (const reference of tsconfig.references ?? []) {
159
+ const filename = path.basename(reference.path);
160
+ expect(libRootAllowedFiles.has(filename), `libRoot template references "${reference.path}"`).toBe(true);
161
+ }
162
+ });
163
+
164
+ test("pkgRoot and workspaceRoot templates declare no references", async () => {
165
+ for (const dir of ["pkgRoot", "workspaceRoot"]) {
166
+ const tsconfig = await loadRootTemplate(dir);
167
+ expect(tsconfig.references, `${dir} template must not declare references`).toBeUndefined();
168
+ }
169
+ });
170
+ });
@@ -84,7 +84,24 @@ const libFacetRootTestPattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(
84
84
  export const isAllowedLibFacetRootFile = (filename: string) =>
85
85
  libFacetRootAllowedFiles.has(filename) || libFacetRootTestPattern.test(filename);
86
86
 
87
- //* What a Capacitor app kept in its root. The native runtime generates its projects under `.akan/native/<target>`, so
87
+ export const barrelFacetDirs = ["ui", "webkit", "srvkit", "common", "plugin"] as const;
88
+
89
+ //* Bun resolves a folder import to `index.tsx`, `index.jsx` or `index.mts` ahead of `index.ts`, and TypeScript to
90
+ //* `index.ts` first, so one of these beside the generated barrel typechecks every name the barrel adds and serves none.
91
+ const barrelShadowingIndexFiles = new Set(["index.tsx", "index.jsx", "index.mts"]);
92
+
93
+ export const facetIndexShadowOf = (sysRelativePath: string) => {
94
+ const [facet, filename = "", ...nested] = sysRelativePath.split("/");
95
+ if (nested.length || !barrelShadowingIndexFiles.has(filename) || !barrelFacetDirs.some((dir) => dir === facet))
96
+ return null;
97
+ return (
98
+ `shadows the generated ${facet}/index.ts: Bun loads ${filename} and TypeScript checks index.ts, so a name only ` +
99
+ `the generated barrel exports typechecks and is undefined at runtime. Delete it, since the generated barrel ` +
100
+ `re-exports every file and folder in ${facet}/; a namespace it builds belongs in ${facet}/<Folder>/index.tsx`
101
+ );
102
+ };
103
+
104
+ //* What a Capacitor app kept in its root. The native runtime generates its projects under `dist/native/<app>/<target>`, so
88
105
  //* these are named as leftovers instead of as unknown entries.
89
106
  const retiredAppRootEntries = new Set([
90
107
  "android",
@@ -97,7 +114,7 @@ const retiredAppRootEntries = new Set([
97
114
 
98
115
  export const rootEntryHintOf = (type: SysType, name: string) =>
99
116
  type === "app" && retiredAppRootEntries.has(name)
100
- ? "a Capacitor-era entry; the native runtime generates its projects under .akan/native/<target>, so delete it"
117
+ ? "a Capacitor-era entry; the native runtime generates its projects under dist/native/<app>/<target>, so delete it"
101
118
  : null;
102
119
 
103
120
  // scanSync reads roots through `Bun.Glob("*")`, which skips dotfiles, so doctor skips them the same way.