@akanjs/devkit 3.0.0-alpha.85 → 3.0.0-alpha.86

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/biome.base.json CHANGED
@@ -60,10 +60,10 @@
60
60
  }
61
61
  },
62
62
  "domains": {
63
- "project": "none",
63
+ "project": "recommended",
64
64
  "react": "recommended",
65
65
  "test": "recommended",
66
- "types": "none"
66
+ "types": "all"
67
67
  }
68
68
  },
69
69
  "javascript": {
package/biomeBase.ts CHANGED
@@ -1,16 +1,9 @@
1
1
  /** `extends` target for a workspace `biome.json`; Biome resolves it through node_modules. */
2
2
  export const biomeBaseConfig = "@akanjs/devkit/biome.base.json";
3
3
 
4
- // The `types` and `project` domains are the two that make Biome build a type and a module graph, so `biome.base.json`
5
- // leaves them `none` and the editor never pays for them on save. They live here instead, applied on top of the base
6
- // by the batch runners through `BiomeStrictConfig`. Omitting a domain is not the same as `none` — Biome infers an
7
- // unspecified domain from the project's dependencies and turns it on — so both files spell every domain out.
8
- export const biomeDomainsConfig = "@akanjs/devkit/biome.domains.json";
9
-
10
4
  // Biome moves rules between groups across minors — `noUnnecessaryConditions` sat in `nursery` at 2.4 and moved to
11
5
  // `suspicious` at 2.5 — and the stale position is a hard "unknown key" error, not a warning. A workspace whose
12
6
  // Biome disagrees with the shipped base config therefore fails to load it at all, which is why the version is
13
- // pinned here instead of resolved to latest at create time. Bump this, `biome.base.json` and `biome.domains.json`
14
- // in one commit, and run `biome migrate --write` in the workspace root and in `pkgs/@akanjs/devkit` so every config
15
- // moves together.
7
+ // pinned here instead of resolved to latest at create time. Bump this and `biome.base.json` in one commit, and run
8
+ // `biome migrate --write` in the workspace root and in `pkgs/@akanjs/devkit` so both configs move together.
16
9
  export const biomeVersion = "2.5.12";
@@ -377,7 +377,7 @@ export class SourceMtimeIndex {
377
377
  return !name || name.startsWith(".") || name === "node_modules";
378
378
  }
379
379
 
380
- /** `WatchRootResolver` can return `apps/<app>/page` alongside `apps/`; walking both doubles the work. */
380
+ /** `WatchRootResolver` can return `apps/<app>/page` alongside `apps/<app>`; walking both doubles the work. */
381
381
  static #pruneNestedRoots(roots: string[]): string[] {
382
382
  const resolved = [...new Set(roots.map((root) => path.resolve(root)))].sort();
383
383
  return resolved.filter(
@@ -0,0 +1,67 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import type { App } from "../commandDecorators";
6
+ import { WatchRootResolver } from "./watchRootResolver";
7
+
8
+ const roots: string[] = [];
9
+
10
+ const makeWorkspace = async (dirs: string[]) => {
11
+ const root = await mkdtemp(path.join(os.tmpdir(), "akan-watch-roots-"));
12
+ roots.push(root);
13
+ await Promise.all(dirs.map((dir) => mkdir(path.join(root, dir), { recursive: true })));
14
+ return root;
15
+ };
16
+
17
+ const makeApp = (workspaceRoot: string, name: string, paths: Record<string, string[]>) =>
18
+ ({
19
+ cwdPath: path.join(workspaceRoot, "apps", name),
20
+ workspace: { workspaceRoot },
21
+ getTsConfig: async () => ({ compilerOptions: { paths } }),
22
+ }) as unknown as App;
23
+
24
+ afterEach(async () => {
25
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
26
+ });
27
+
28
+ describe("WatchRootResolver", () => {
29
+ test("narrows the apps container to the app being served", async () => {
30
+ const workspaceRoot = await makeWorkspace(["apps/app1/page", "apps/app2/page", "libs/util"]);
31
+ const resolved = await new WatchRootResolver(
32
+ makeApp(workspaceRoot, "app1", { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] }),
33
+ ).resolve();
34
+
35
+ expect(resolved).toContain(path.join(workspaceRoot, "apps/app1"));
36
+ expect(resolved).not.toContain(path.join(workspaceRoot, "apps"));
37
+ expect(resolved).not.toContain(path.join(workspaceRoot, "apps/app2"));
38
+ });
39
+
40
+ test("keeps the libs container whole", async () => {
41
+ const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
42
+ const resolved = await new WatchRootResolver(makeApp(workspaceRoot, "app1", { "@libs/*": ["./libs/*"] })).resolve();
43
+
44
+ expect(resolved).toContain(path.join(workspaceRoot, "libs"));
45
+ });
46
+
47
+ test("resolves a package alias to its own directory, filename and glob stripped", async () => {
48
+ const workspaceRoot = await makeWorkspace(["apps/app1/page", "pkgs/akanjs/base"]);
49
+ const resolved = await new WatchRootResolver(
50
+ makeApp(workspaceRoot, "app1", {
51
+ akanjs: ["./pkgs/akanjs/index.ts"],
52
+ "akanjs/*": ["./pkgs/akanjs/*"],
53
+ missing: ["./pkgs/nothing/index.ts"],
54
+ }),
55
+ ).resolve();
56
+
57
+ expect(resolved).toContain(path.join(workspaceRoot, "pkgs/akanjs"));
58
+ expect(resolved).not.toContain(path.join(workspaceRoot, "pkgs/nothing"));
59
+ });
60
+
61
+ test("always watches the app's page tree", async () => {
62
+ const workspaceRoot = await makeWorkspace(["apps/app1/page"]);
63
+ const resolved = await new WatchRootResolver(makeApp(workspaceRoot, "app1", {})).resolve();
64
+
65
+ expect(resolved).toEqual([path.join(workspaceRoot, "apps/app1/page")]);
66
+ });
67
+ });
@@ -2,6 +2,15 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { App } from "../commandDecorators";
4
4
 
5
+ /**
6
+ * The directories one app's dev watcher follows, resolved from its tsconfig `paths`.
7
+ *
8
+ * `@apps/*` strips down to the `apps/` container, so taking it verbatim puts every sibling app under the
9
+ * watcher: with two dev servers up, a save in app2 rebuilds, restarts and reloads app1, and both builders
10
+ * rewrite the same generated barrels. Apps are leaves of the workspace graph — never one another's
11
+ * dependencies — so the container is replaced by this app's own directory. `libs/` stays whole, because a
12
+ * lib becomes a dependency the moment someone types the import, with no restart in between.
13
+ */
5
14
  export class WatchRootResolver {
6
15
  #app: App;
7
16
 
@@ -11,6 +20,8 @@ export class WatchRootResolver {
11
20
 
12
21
  async resolve(): Promise<string[]> {
13
22
  const tsconfig = await this.#app.getTsConfig();
23
+ const appDir = path.resolve(this.#app.cwdPath);
24
+ const appsContainer = path.dirname(appDir);
14
25
  const set = new Set<string>();
15
26
  set.add(path.resolve(`${this.#app.cwdPath}/page`));
16
27
  for (const targets of Object.values(tsconfig.compilerOptions.paths ?? {})) {
@@ -20,7 +31,8 @@ export class WatchRootResolver {
20
31
  // Strip the trailing filename and glob so we watch the package root dir.
21
32
  const cleaned = target.replace(/\/?\*+.*$/, "").replace(/\/[^/]+\.[^/]+$/, "");
22
33
  const resolved = path.resolve(this.#app.workspace.workspaceRoot, cleaned);
23
- if (fs.existsSync(resolved)) set.add(resolved);
34
+ const root = resolved === appsContainer ? appDir : resolved;
35
+ if (fs.existsSync(root)) set.add(root);
24
36
  }
25
37
  }
26
38
  return [...set];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.0.0-alpha.85",
3
+ "version": "3.0.0-alpha.86",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -24,7 +24,6 @@
24
24
  },
25
25
  "./package.json": "./package.json",
26
26
  "./biome.base.json": "./biome.base.json",
27
- "./biome.domains.json": "./biome.domains.json",
28
27
  "./akanApp": "./akanApp/index.ts",
29
28
  "./akanConfig": "./akanConfig/index.ts",
30
29
  "./artifact": "./artifact/index.ts",
@@ -46,7 +45,7 @@
46
45
  "@langchain/openai": "^1.4.6",
47
46
  "@tailwindcss/node": "^4.3.0",
48
47
  "@trapezedev/project": "^7.1.4",
49
- "akanjs": "3.0.0-alpha.85",
48
+ "akanjs": "3.0.0-alpha.86",
50
49
  "chalk": "^5.6.2",
51
50
  "commander": "^14.0.3",
52
51
  "dayjs": "^1.11.20",
@@ -1,9 +0,0 @@
1
- {
2
- "$schema": "https://biomejs.dev/schemas/2.5.12/schema.json",
3
- "linter": {
4
- "domains": {
5
- "project": "recommended",
6
- "types": "all"
7
- }
8
- }
9
- }
@@ -1,54 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { mkdtemp } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import path from "node:path";
5
- import { BiomeStrictConfig } from "./biomeStrictConfig";
6
-
7
- const workspace = async () => await mkdtemp(path.join(tmpdir(), "akan-biome-strict-"));
8
-
9
- describe("BiomeStrictConfig", () => {
10
- test("writes nothing when the workspace has no biome config", async () => {
11
- const root = await workspace();
12
- const strictConfig = new BiomeStrictConfig(root);
13
-
14
- expect(await strictConfig.write()).toBeNull();
15
- expect(await Bun.file(strictConfig.filePath).exists()).toBe(false);
16
- });
17
-
18
- test("layers the domains over the workspace config, base first", async () => {
19
- const root = await workspace();
20
- await Bun.write(path.join(root, "biome.json"), "{}\n");
21
- const strictConfig = new BiomeStrictConfig(root, { id: "test" });
22
-
23
- const filePath = await strictConfig.write();
24
-
25
- expect(filePath).toBe(path.join(root, ".biome.strict.test.json"));
26
- expect(await Bun.file(path.join(root, ".biome.strict.test.json")).json()).toEqual({
27
- extends: ["@akanjs/devkit/biome.base.json", "./biome.json", "@akanjs/devkit/biome.domains.json"],
28
- });
29
- });
30
-
31
- test("prefers biome.json over biome.jsonc, mirroring Biome's own precedence", async () => {
32
- const root = await workspace();
33
- await Bun.write(path.join(root, "biome.json"), "{}\n");
34
- await Bun.write(path.join(root, "biome.jsonc"), "{}\n");
35
- const strictConfig = new BiomeStrictConfig(root, { id: "test" });
36
-
37
- await strictConfig.write();
38
-
39
- const written = (await Bun.file(strictConfig.filePath).json()) as { extends: string[] };
40
- expect(written.extends[1]).toBe("./biome.json");
41
- });
42
-
43
- test("removes the copy, and stays quiet when there is nothing to remove", async () => {
44
- const root = await workspace();
45
- await Bun.write(path.join(root, "biome.jsonc"), "{}\n");
46
- const strictConfig = new BiomeStrictConfig(root, { id: "test" });
47
- await strictConfig.write();
48
-
49
- await strictConfig.remove();
50
- await strictConfig.remove();
51
-
52
- expect(await Bun.file(strictConfig.filePath).exists()).toBe(false);
53
- });
54
- });
@@ -1,58 +0,0 @@
1
- import { rm } from "node:fs/promises";
2
- import path from "node:path";
3
- import { biomeBaseConfig, biomeDomainsConfig } from "./biomeBase";
4
-
5
- /**
6
- * The editor and the batch runs want different `linter.domains`, and Biome has no per-invocation switch for them:
7
- * `--only` / `--skip` pick which rules run, not whether the type and module graphs are built, and `--only=<domain>`
8
- * additionally force-enables every rule of that domain, including the ones the shared config deliberately leaves
9
- * off. So the domains are a configuration difference, and this class is the batch half of it — a throwaway config
10
- * that re-applies `biome.domains.json` over the workspace's own, used by `akan lint` and the pre-commit hook.
11
- *
12
- * Two details fix its shape. It has to sit in the workspace root, because Biome resolves `plugins` paths from the
13
- * entry configuration's own directory — a config under `.husky/` loads none of the grit rules and reports only
14
- * "Cannot read file". And it has to name `biome.base.json` itself rather than lean on the workspace config's own
15
- * `extends`, because `extends` is one level deep: a config extending `./biome.json` inherits `biome.json`'s own
16
- * keys and nothing that `biome.json` in turn extends. Listing the base first keeps `overrides` in the order a
17
- * plain run has them, workspace entries after the framework's.
18
- */
19
- export class BiomeStrictConfig {
20
- static readonly configFileNames = ["biome.json", "biome.jsonc"] as const;
21
-
22
- /** `biome.json` first, mirroring Biome's own precedence; `biome.jsonc` is the one that may carry comments. */
23
- static async resolveConfigName(workspaceRoot: string): Promise<string | null> {
24
- for (const fileName of BiomeStrictConfig.configFileNames) {
25
- if (await Bun.file(path.join(workspaceRoot, fileName)).exists()) return fileName;
26
- }
27
- return null;
28
- }
29
-
30
- readonly #filePath: string;
31
- readonly #workspaceRoot: string;
32
-
33
- constructor(workspaceRoot: string, { id = process.pid }: { id?: number | string } = {}) {
34
- this.#workspaceRoot = workspaceRoot;
35
- this.#filePath = path.join(workspaceRoot, `.biome.strict.${id}.json`);
36
- }
37
-
38
- get filePath() {
39
- return this.#filePath;
40
- }
41
-
42
- async write(): Promise<string | null> {
43
- const configName = await BiomeStrictConfig.resolveConfigName(this.#workspaceRoot);
44
- if (!configName) return null;
45
- const config = { extends: [biomeBaseConfig, `./${configName}`, biomeDomainsConfig] };
46
- await Bun.write(this.#filePath, `${JSON.stringify(config, null, 2)}\n`);
47
- return this.#filePath;
48
- }
49
-
50
- async remove() {
51
- await rm(this.#filePath, { force: true });
52
- }
53
- }
54
-
55
- if (import.meta.main) {
56
- const filePath = await new BiomeStrictConfig(process.cwd()).write();
57
- if (filePath) process.stdout.write(path.relative(process.cwd(), filePath));
58
- }