@akanjs/devkit 3.1.1-rc.1 → 3.1.1-rc.10

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 (37) hide show
  1. package/akanConfig/akanConfig.test.ts +13 -2
  2. package/akanConfig/akanConfig.ts +5 -5
  3. package/akanConfig/productionLockfile.test.ts +80 -0
  4. package/akanConfig/productionLockfile.ts +32 -0
  5. package/akanContext.ts +25 -0
  6. package/applicationBuildRunner.test.ts +41 -0
  7. package/applicationBuildRunner.ts +23 -3
  8. package/biomeBase.test.ts +2 -2
  9. package/cloud/cloudApi.ts +10 -4
  10. package/cloud/cloudHttpError.test.ts +8 -0
  11. package/cloud/cloudHttpError.ts +11 -1
  12. package/cloud/cloudUpdates.ts +48 -2
  13. package/cloud/updatePackUpload.test.ts +38 -2
  14. package/cloud/updatePackUpload.ts +5 -1
  15. package/commandDecorators/command.ts +3 -14
  16. package/commandDecorators/commandDecorators.test.ts +1 -1
  17. package/commandDecorators/workspaceVersionCheck.test.ts +70 -0
  18. package/commandDecorators/workspaceVersionCheck.ts +50 -0
  19. package/executors.test.ts +21 -1
  20. package/lint/gritRules.test.ts +3 -3
  21. package/linter.test.ts +18 -1
  22. package/linter.ts +10 -6
  23. package/mobile/desktopRelease.test.ts +473 -0
  24. package/mobile/desktopRelease.ts +307 -0
  25. package/mobile/desktopServerStage.ts +4 -2
  26. package/mobile/index.ts +2 -0
  27. package/mobile/nativeApi.test.ts +4 -1
  28. package/mobile/nativeApi.ts +3 -3
  29. package/mobile/nativeApp.test.ts +2 -2
  30. package/mobile/nativeApp.ts +29 -5
  31. package/mobile/nativeConfig.test.ts +16 -8
  32. package/mobile/nativeConfig.ts +1 -1
  33. package/mobile/tarWriter.test.ts +89 -0
  34. package/mobile/tarWriter.ts +93 -0
  35. package/mobile/updateUploader.test.ts +34 -0
  36. package/mobile/updateUploader.ts +9 -6
  37. package/package.json +2 -2
@@ -0,0 +1,70 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdir, symlink } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { tempDirs, writeJson } from "../testHelpers";
5
+ import { WorkspaceVersionCheck } from "./workspaceVersionCheck";
6
+
7
+ const makeRoot = tempDirs("akan-version-check-");
8
+ const argv = (...args: string[]) => ["bun", "akan", ...args];
9
+
10
+ const makeWorkspace = async ({ akanjs, cli }: { akanjs?: string; cli?: string }) => {
11
+ const root = await makeRoot();
12
+ if (akanjs) await writeJson(path.join(root, "node_modules/akanjs/package.json"), { name: "akanjs", version: akanjs });
13
+ if (cli)
14
+ await writeJson(path.join(root, "node_modules/@akanjs/cli/package.json"), { name: "@akanjs/cli", version: cli });
15
+ return root;
16
+ };
17
+
18
+ describe("WorkspaceVersionCheck", () => {
19
+ test("stays quiet when the CLI and the workspace's akanjs match, or when akanjs is not installed", async () => {
20
+ expect(
21
+ WorkspaceVersionCheck.warning("3.1.1-rc.5", argv("sync", "demo"), await makeWorkspace({ akanjs: "3.1.1-rc.5" })),
22
+ ).toBeNull();
23
+ expect(WorkspaceVersionCheck.warning("3.1.1-rc.5", argv("sync", "demo"), await makeWorkspace({}))).toBeNull();
24
+ });
25
+
26
+ test("names both versions and the matching CLI with the same arguments", async () => {
27
+ const root = await makeWorkspace({ akanjs: "3.1.0-rc.6" });
28
+
29
+ const warning = WorkspaceVersionCheck.warning("3.1.0", argv("sync", "demo", "--title", "it's"), root);
30
+
31
+ expect(warning).toContain("@akanjs/cli 3.1.0, but this workspace has akanjs 3.1.0-rc.6");
32
+ expect(warning).toContain(`Run the matching CLI: bunx -p @akanjs/cli@3.1.0-rc.6 akan sync demo --title 'it'\\''s'`);
33
+ expect(warning).toEndWith("together: akan update");
34
+ });
35
+
36
+ test("offers akan update with the CLI's prerelease tag only when the CLI is the newer one", async () => {
37
+ const older = await makeWorkspace({ akanjs: "3.1.1-rc.4" });
38
+ expect(WorkspaceVersionCheck.warning("3.1.1-rc.5", argv("build", "demo"), older)).toEndWith("akan update --tag rc");
39
+
40
+ const newer = await makeWorkspace({ akanjs: "3.1.1-rc.5" });
41
+ const warning = WorkspaceVersionCheck.warning("3.1.0", argv("build", "demo"), newer);
42
+ expect(warning).not.toContain("akan update");
43
+ expect(warning).toEndWith("Pin it so `bunx akan` runs it: bun add -d @akanjs/cli@3.1.1-rc.5");
44
+ });
45
+
46
+ test("points at the workspace's own pinned CLI when it matches akanjs", async () => {
47
+ const root = await makeWorkspace({ akanjs: "3.1.1-rc.5", cli: "3.1.1-rc.5" });
48
+
49
+ const warning = WorkspaceVersionCheck.warning("3.1.0", argv("start", "demo"), root);
50
+
51
+ expect(warning).toContain("Run the matching CLI: bunx akan start demo");
52
+ expect(warning).not.toContain("bun add -d");
53
+ });
54
+
55
+ test("leaves update, create-workspace, help and version alone", async () => {
56
+ const root = await makeWorkspace({ akanjs: "3.1.0-rc.6" });
57
+ for (const args of [["update"], ["create-workspace", "next"], ["--version"], ["-V"], ["sync", "--help"]])
58
+ expect(WorkspaceVersionCheck.warning("3.1.0", argv(...args), root)).toBeNull();
59
+ });
60
+
61
+ test("skips a workspace that links akanjs from its own source", async () => {
62
+ const root = await makeWorkspace({});
63
+ await writeJson(path.join(root, "pkgs/akanjs/package.json"), { name: "akanjs", version: "3.1.1-rc.4" });
64
+ await mkdir(path.join(root, "node_modules"), { recursive: true });
65
+ await symlink(path.join(root, "pkgs/akanjs"), path.join(root, "node_modules/akanjs"), "dir");
66
+
67
+ expect(WorkspaceVersionCheck.installedAkanVersion(root)).toBeNull();
68
+ expect(WorkspaceVersionCheck.warning("3.1.0", argv("sync", "demo"), root)).toBeNull();
69
+ });
70
+ });
@@ -0,0 +1,50 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { PackageJson } from "../types";
4
+
5
+ export class WorkspaceVersionCheck {
6
+ static readonly #exemptCommands = new Set(["update", "create-workspace"]);
7
+ static readonly #infoFlags = new Set(["--help", "-h", "--version", "-V"]);
8
+ static readonly #updateTags = new Set(["dev", "canary", "beta", "rc", "alpha"]);
9
+
10
+ static warning(cliVersion: string | undefined, argv: string[], workspaceRoot: string) {
11
+ const args = argv.slice(2);
12
+ if (!cliVersion || WorkspaceVersionCheck.#exemptCommands.has(args[0] ?? "")) return null;
13
+ if (args.some((arg) => WorkspaceVersionCheck.#infoFlags.has(arg))) return null;
14
+ const installed = WorkspaceVersionCheck.installedAkanVersion(workspaceRoot);
15
+ if (!installed || installed === cliVersion) return null;
16
+ const pinnedCli = WorkspaceVersionCheck.#versionAt(path.join(workspaceRoot, "node_modules/@akanjs/cli"));
17
+ const runner = pinnedCli === installed ? ["bunx"] : ["bunx", "-p", `@akanjs/cli@${installed}`];
18
+ const rerun = [...runner, "akan", ...args].map(WorkspaceVersionCheck.#quote).join(" ");
19
+ const [, prerelease = ""] = cliVersion.split("-");
20
+ const tag = prerelease.split(".")[0];
21
+ const update = `akan update${WorkspaceVersionCheck.#updateTags.has(tag) ? ` --tag ${tag}` : ""}`;
22
+ const lines = [
23
+ `This akan is @akanjs/cli ${cliVersion}, but this workspace has akanjs ${installed}: what it generates may not match the installed framework.`,
24
+ `Run the matching CLI: ${rerun}`,
25
+ ];
26
+ if (Bun.semver.order(cliVersion, installed) > 0)
27
+ lines.push(`Or move the workspace and this CLI to the newest release of the line together: ${update}`);
28
+ else if (pinnedCli !== installed)
29
+ lines.push(`Pin it so \`bunx akan\` runs it: bun add -d @akanjs/cli@${installed}`);
30
+ return lines.join("\n");
31
+ }
32
+
33
+ static installedAkanVersion(workspaceRoot: string) {
34
+ const packageDir = path.join(workspaceRoot, "node_modules", "akanjs");
35
+ if (!existsSync(packageDir)) return null;
36
+ //? A workspace link (the framework's own repo) runs akanjs from source, so no installed release can differ.
37
+ if (!realpathSync(packageDir).split(path.sep).includes("node_modules")) return null;
38
+ return WorkspaceVersionCheck.#versionAt(packageDir);
39
+ }
40
+
41
+ static #versionAt(packageDir: string) {
42
+ const packageJsonPath = path.join(packageDir, "package.json");
43
+ if (!existsSync(packageJsonPath)) return null;
44
+ return (JSON.parse(readFileSync(packageJsonPath, "utf8")) as Partial<PackageJson>).version ?? null;
45
+ }
46
+
47
+ static #quote(arg: string) {
48
+ return /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
49
+ }
50
+ }
package/executors.test.ts CHANGED
@@ -1383,7 +1383,13 @@ describe("SysExecutor scan", () => {
1383
1383
  expect(await lib.readFile("srvkit/index.ts")).toBe(
1384
1384
  ["a9", "a10", "aa", "aB"].map((name) => `export * from "./${name}";\n`).join(""),
1385
1385
  );
1386
- for (const file of ["ui/index.ts", "srvkit/index.ts", "lib/post/index.ts", "akan.lib.json"]) {
1386
+ for (const file of [
1387
+ "ui/index.ts",
1388
+ "srvkit/index.ts",
1389
+ "lib/post/index.ts",
1390
+ "akan.lib.json",
1391
+ "env/env.server.type.ts",
1392
+ ]) {
1387
1393
  const generated = await lib.readFile(file);
1388
1394
  expect(await formatWithBiome(generated, `libs/biomestable/${file}`)).toBe(generated);
1389
1395
  }
@@ -1392,6 +1398,20 @@ describe("SysExecutor scan", () => {
1392
1398
  expect(await devSync.syncForBatch(events)).toEqual({ changedFiles: [], errors: [] });
1393
1399
  });
1394
1400
 
1401
+ test.skipIf(!hasBiome)(
1402
+ "writes env.server.type.ts the way Biome prints it, spreading libs in dependency order",
1403
+ async () => {
1404
+ const { default: getContent } = (await import("../cli/templates/env/_env.server.type")) as {
1405
+ default: (scanInfo: unknown) => { content: string };
1406
+ };
1407
+ const libs = ["util", "shared", "shopAdmin", "shop", "a10", "a9"];
1408
+ const { content } = getContent({ getLibs: () => libs });
1409
+
1410
+ expect(await formatWithBiome(content, "apps/biomestable/env/env.server.type.ts")).toBe(content);
1411
+ expect([...content.matchAll(/\.\.\.(\w+)Option/g)].map(([, lib]) => lib)).toEqual(libs);
1412
+ },
1413
+ );
1414
+
1395
1415
  test("refuses a hand-written ui/index.tsx beside the generated barrel, naming the file and the fix", async () => {
1396
1416
  const { lib } = await createScannableLib("shadowedbarrel", {
1397
1417
  files: {
@@ -11,7 +11,7 @@ import { Linter } from "../linter";
11
11
  const lintDir = import.meta.dir;
12
12
  const fixturesDir = path.join(lintDir, "__fixtures__");
13
13
  const workspaceRoot = path.resolve(lintDir, "../../../..");
14
- const biomeBin = path.join(workspaceRoot, "node_modules", ".bin", Linter.biomeBinName);
14
+ const [biome, ...biomeArgs] = Linter.biomeCommand(workspaceRoot);
15
15
 
16
16
  interface BiomeDiagnostic {
17
17
  severity: string;
@@ -42,7 +42,7 @@ afterAll(async () => {
42
42
 
43
43
  const runBiome = (args: string[], cwd: string) =>
44
44
  new Promise<string>((resolve, reject) => {
45
- const proc = spawn(biomeBin, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
45
+ const proc = spawn(biome, [...biomeArgs, ...args], { cwd, stdio: ["ignore", "pipe", "pipe"] });
46
46
  let stdout = "";
47
47
  let stderr = "";
48
48
  proc.stdout.on("data", (chunk: Buffer) => (stdout += chunk.toString()));
@@ -127,7 +127,7 @@ const fixtures = await collectCases();
127
127
 
128
128
  describe("grit lint rules", () => {
129
129
  test("the harness found the biome binary and at least one fixture", () => {
130
- expect(existsSync(biomeBin)).toBe(true);
130
+ expect(biome).toBe(process.execPath);
131
131
  expect(fixtures.length).toBeGreaterThan(0);
132
132
  });
133
133
 
package/linter.test.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { type BiomeReport, parseBiomeReport } from "./linter";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { type BiomeReport, Linter, parseBiomeReport } from "./linter";
5
+ import { hasBiome } from "./testHelpers";
3
6
 
4
7
  const reportOf = (diagnostics: BiomeReport["diagnostics"]): string =>
5
8
  JSON.stringify({ summary: { errors: diagnostics?.length ?? 0, warnings: 0 }, diagnostics });
@@ -77,3 +80,17 @@ describe("parseBiomeReport", () => {
77
80
  expect(() => parseBiomeReport(" \n")).toThrow("No Biome JSON output");
78
81
  });
79
82
  });
83
+
84
+ describe("Linter", () => {
85
+ test.skipIf(!hasBiome || process.platform === "win32")("runs Biome where no node is on PATH", async () => {
86
+ const pathBefore = process.env.PATH;
87
+ process.env.PATH = path.join(tmpdir(), "akan-path-without-node");
88
+ try {
89
+ const { results, errors } = await new Linter(import.meta.dir).lintFile(path.join(import.meta.dir, "linter.ts"));
90
+ expect(results).toHaveLength(1);
91
+ expect(errors).toEqual([]);
92
+ } finally {
93
+ process.env.PATH = pathBefore;
94
+ }
95
+ });
96
+ });
package/linter.ts CHANGED
@@ -113,18 +113,21 @@ const emptyResult = (filePath: string): LintResult => ({
113
113
  });
114
114
 
115
115
  export class Linter {
116
- //? Bun links a package bin as `<name>.exe` (plus a `.bunx` stub) on Windows, and a bare `<name>` elsewhere.
117
- static readonly biomeBinName = process.platform === "win32" ? "biome.exe" : "biome";
116
+ //? The package's bin is npm's `#!/usr/bin/env node` wrapper, which picks the platform binary; run through the Bun
117
+ //? running akan, it works in an image with no Node (exit 127 otherwise). A Biome on PATH is the fallback.
118
+ static biomeCommand(root: string): [string, ...string[]] {
119
+ const wrapper = path.join(root, "node_modules", "@biomejs", "biome", "bin", "biome");
120
+ return existsSync(wrapper) ? [process.execPath, wrapper] : ["biome"];
121
+ }
118
122
 
119
123
  lintRoot: string;
120
124
  configPath: string;
121
- #biomeBin: string;
125
+ #biomeCommand: [string, ...string[]];
122
126
 
123
127
  constructor(cwdPath: string) {
124
128
  this.lintRoot = this.#findBiomeRootPath(cwdPath);
125
129
  this.configPath = Linter.#configPathIn(this.lintRoot) ?? path.join(this.lintRoot, "biome.json");
126
- const localBiomeBin = path.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
127
- this.#biomeBin = existsSync(localBiomeBin) ? localBiomeBin : "biome";
130
+ this.#biomeCommand = Linter.biomeCommand(this.lintRoot);
128
131
  }
129
132
 
130
133
  static #configPathIn(dir: string): string | null {
@@ -158,7 +161,8 @@ export class Linter {
158
161
  stderr: string;
159
162
  code: number | null;
160
163
  }>((resolve, reject) => {
161
- const proc = spawn(this.#biomeBin, args, {
164
+ const [command, ...prefixArgs] = this.#biomeCommand;
165
+ const proc = spawn(command, [...prefixArgs, ...args], {
162
166
  cwd: this.lintRoot,
163
167
  stdio: ["pipe", "pipe", "pipe"],
164
168
  });