@akanjs/devkit 3.1.1-rc.1 → 3.1.1-rc.11
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/akanApp/akanApp.host.test.ts +12 -0
- package/akanApp/akanApp.host.ts +12 -0
- package/akanConfig/akanConfig.test.ts +43 -2
- package/akanConfig/akanConfig.ts +15 -5
- package/akanConfig/productionLockfile.test.ts +111 -0
- package/akanConfig/productionLockfile.ts +34 -0
- package/akanContext.ts +25 -0
- package/applicationBuildRunner.test.ts +41 -0
- package/applicationBuildRunner.ts +23 -3
- package/biomeBase.test.ts +2 -2
- package/cloud/cloudApi.ts +10 -4
- package/cloud/cloudHttpError.test.ts +8 -0
- package/cloud/cloudHttpError.ts +11 -1
- package/cloud/cloudUpdates.ts +48 -2
- package/cloud/updatePackUpload.test.ts +38 -2
- package/cloud/updatePackUpload.ts +5 -1
- package/commandDecorators/command.ts +3 -14
- package/commandDecorators/commandDecorators.test.ts +1 -1
- package/commandDecorators/workspaceVersionCheck.test.ts +80 -0
- package/commandDecorators/workspaceVersionCheck.ts +53 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
- package/executors.test.ts +21 -1
- package/integration/devStability.integration.test.ts +9 -5
- package/lint/gritRules.test.ts +3 -3
- package/linter.test.ts +18 -1
- package/linter.ts +10 -6
- package/mobile/desktopRelease.test.ts +473 -0
- package/mobile/desktopRelease.ts +309 -0
- package/mobile/desktopServerStage.ts +4 -2
- package/mobile/index.ts +2 -0
- package/mobile/nativeApi.test.ts +4 -1
- package/mobile/nativeApi.ts +3 -3
- package/mobile/nativeApp.test.ts +2 -2
- package/mobile/nativeApp.ts +29 -5
- package/mobile/nativeConfig.test.ts +16 -8
- package/mobile/nativeConfig.ts +1 -1
- package/mobile/tarWriter.test.ts +89 -0
- package/mobile/tarWriter.ts +93 -0
- package/mobile/updateUploader.test.ts +34 -0
- package/mobile/updateUploader.ts +9 -6
- package/package.json +2 -2
- package/platformTest/LinuxDockerTarget.ts +3 -0
- package/signalMutationIndex.test.ts +15 -0
- package/signalMutationIndex.ts +1 -1
- package/types.ts +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { truncate, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { Workspace } from "../commandDecorators";
|
|
6
6
|
import { tempDirs } from "../testHelpers";
|
|
@@ -24,6 +24,7 @@ class FakeCloudHost {
|
|
|
24
24
|
commitAnswers: (number | [number, string] | Response)[] = [];
|
|
25
25
|
release: Record<string, unknown> = { id: "release-1", status: "published", problems: [] };
|
|
26
26
|
requestRefusal: Response | null = null;
|
|
27
|
+
contentType = "application/gzip";
|
|
27
28
|
|
|
28
29
|
constructor() {
|
|
29
30
|
this.server = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: (request) => this.#route(request) });
|
|
@@ -42,6 +43,7 @@ class FakeCloudHost {
|
|
|
42
43
|
this.stallPuts = false;
|
|
43
44
|
this.commitAnswers = [];
|
|
44
45
|
this.requestRefusal = null;
|
|
46
|
+
this.contentType = "application/gzip";
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
async #route(request: Request) {
|
|
@@ -54,7 +56,7 @@ class FakeCloudHost {
|
|
|
54
56
|
uploadId,
|
|
55
57
|
uploadUrl: `${this.host}/storage/${uploadId}?X-Amz-Signature=secret-signature`,
|
|
56
58
|
expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
|
|
57
|
-
contentType:
|
|
59
|
+
contentType: this.contentType,
|
|
58
60
|
});
|
|
59
61
|
}
|
|
60
62
|
if (url.pathname.startsWith("/storage/") && request.method === "PUT") {
|
|
@@ -302,4 +304,38 @@ describe("UpdatePackUpload", () => {
|
|
|
302
304
|
await expect(new UpdatePackUpload(api, "project-1", quick).send(archive, fields)).rejects.toThrow("0 bytes");
|
|
303
305
|
expect(cloudHost.requests).toEqual([]);
|
|
304
306
|
});
|
|
307
|
+
|
|
308
|
+
test("announces a desktop release with its kind and CPU, a phone pack with neither, and PUTs the ticket's type", async () => {
|
|
309
|
+
const desktop = { ...fields, platform: "macos" as const, kind: "app" as const, arch: "arm64" as const };
|
|
310
|
+
cloudHost.contentType = "application/x-tar";
|
|
311
|
+
|
|
312
|
+
await new UpdatePackUpload(api, "project-1", quick).send(await archiveOf("release tar"), desktop);
|
|
313
|
+
await new UpdatePackUpload(api, "project-1", quick).send(await archiveOf("pack bytes"), fields);
|
|
314
|
+
|
|
315
|
+
expect(cloudHost.requests[0]).toMatchObject({ platform: "macos", kind: "app", arch: "arm64", size: 11 });
|
|
316
|
+
expect(Object.keys(cloudHost.requests[1] ?? {}).filter((key) => key === "kind" || key === "arch")).toEqual([]);
|
|
317
|
+
expect(cloudHost.puts.map((put) => put.headers["content-type"])).toEqual([
|
|
318
|
+
"application/x-tar",
|
|
319
|
+
"application/x-tar",
|
|
320
|
+
]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("takes a desktop release up to 1 GiB and a phone pack up to 200 MiB, refusing more before a ticket", async () => {
|
|
324
|
+
const desktop = { ...fields, platform: "windows" as const, kind: "app" as const, arch: "x64" as const };
|
|
325
|
+
const sparse = async (bytes: number) => {
|
|
326
|
+
const archive = await archiveOf("");
|
|
327
|
+
await truncate(archive, bytes);
|
|
328
|
+
return archive;
|
|
329
|
+
};
|
|
330
|
+
const upload = new UpdatePackUpload(api, "project-1", quick);
|
|
331
|
+
|
|
332
|
+
await expect(upload.send(await sparse(UpdatePackUpload.maxBytes + 1), fields)).rejects.toThrow(
|
|
333
|
+
`The pack archive is ${UpdatePackUpload.maxBytes + 1} bytes; a project takes 1 byte to 200 MiB.`,
|
|
334
|
+
);
|
|
335
|
+
await expect(upload.send(await sparse(UpdatePackUpload.maxAppBytes + 1), desktop)).rejects.toThrow(
|
|
336
|
+
`The release archive is ${UpdatePackUpload.maxAppBytes + 1} bytes; a project takes 1 byte to 1 GiB.`,
|
|
337
|
+
);
|
|
338
|
+
expect(cloudHost.requests).toEqual([]);
|
|
339
|
+
expect(UpdatePackUpload.maxAppBytes).toBe(1_073_741_824);
|
|
340
|
+
});
|
|
305
341
|
});
|
|
@@ -16,6 +16,7 @@ interface UpdatePackUploadOptions {
|
|
|
16
16
|
//* embedded pack; either re-hashes it, and an upload is used once, one way.
|
|
17
17
|
export class UpdatePackUpload {
|
|
18
18
|
static readonly maxBytes = 200 * 1024 * 1024;
|
|
19
|
+
static readonly maxAppBytes = 1024 * 1024 * 1024;
|
|
19
20
|
static readonly #transientPut = new Set([0, 408, 429, 500, 502, 503, 504]);
|
|
20
21
|
//? A stalled PUT never fails on its own, so each try ends once the pack would be through even at this crawl.
|
|
21
22
|
static readonly #putFloorBytesPerSecond = 64 * 1024;
|
|
@@ -57,7 +58,10 @@ export class UpdatePackUpload {
|
|
|
57
58
|
/** Puts the pack in the project's staging storage and answers the upload id a commit or a registration names. */
|
|
58
59
|
async stage(archive: string, fields: Omit<UpdateUploadRequest, "size" | "sha256">): Promise<string> {
|
|
59
60
|
const file = Bun.file(archive);
|
|
60
|
-
if (
|
|
61
|
+
if (fields.kind === "app") {
|
|
62
|
+
if (file.size < 1 || file.size > UpdatePackUpload.maxAppBytes)
|
|
63
|
+
throw new Error(`The release archive is ${file.size} bytes; a project takes 1 byte to 1 GiB.`);
|
|
64
|
+
} else if (file.size < 1 || file.size > UpdatePackUpload.maxBytes)
|
|
61
65
|
throw new Error(`The pack archive is ${file.size} bytes; a project takes 1 byte to 200 MiB.`);
|
|
62
66
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
63
67
|
for await (const chunk of file.stream()) hasher.update(chunk);
|
|
@@ -26,6 +26,7 @@ import { CommandContainer } from "./dependencyBuilder";
|
|
|
26
26
|
import { ExitCodeError } from "./exitCodeError";
|
|
27
27
|
import { formatCommandHelp, formatHelp } from "./helpFormatter";
|
|
28
28
|
import { type CommandCls, getTargetCommandNames, getTargetMetas, type TargetFailure } from "./targetMeta";
|
|
29
|
+
import { WorkspaceVersionCheck } from "./workspaceVersionCheck";
|
|
29
30
|
|
|
30
31
|
const loggedCliErrorObjects = new WeakSet<object>();
|
|
31
32
|
const loggedCliErrorMessages = new Set<string>();
|
|
@@ -374,20 +375,8 @@ export const runCommands = async (...commands: CommandCls[]) => {
|
|
|
374
375
|
program.version(process.env.AKAN_VERSION).description("Akan CLI").configureHelp({
|
|
375
376
|
helpWidth: 100,
|
|
376
377
|
});
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
: null;
|
|
380
|
-
if (installedAkanPackageJson && installedAkanPackageJson.version !== process.env.AKAN_VERSION) {
|
|
381
|
-
Logger.rawLog(
|
|
382
|
-
chalk.yellow(
|
|
383
|
-
`
|
|
384
|
-
Akan CLI version is mismatch with installed package. ${process.env.AKAN_VERSION} (global) vs ${installedAkanPackageJson.version} (akanjs)
|
|
385
|
-
It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
386
|
-
),
|
|
387
|
-
undefined,
|
|
388
|
-
"error",
|
|
389
|
-
);
|
|
390
|
-
}
|
|
378
|
+
const versionWarning = WorkspaceVersionCheck.warning(cliPackageJson?.version, process.argv, process.cwd());
|
|
379
|
+
if (versionWarning) Logger.rawLog(chalk.yellow(`\n${versionWarning}`), undefined, "error");
|
|
391
380
|
|
|
392
381
|
for (const command of commands) {
|
|
393
382
|
const targetMetas = getTargetMetas(command);
|
|
@@ -695,6 +695,6 @@ describe("a workspace whose akanjs is not the CLI's version", () => {
|
|
|
695
695
|
]);
|
|
696
696
|
expect(code).toBe(0);
|
|
697
697
|
expect(stdout).toBe('{"phases":1}');
|
|
698
|
-
expect(stderr).toContain("0.0.1-older
|
|
698
|
+
expect(stderr).toContain("this workspace has akanjs 0.0.1-older");
|
|
699
699
|
}, 30_000);
|
|
700
700
|
});
|
|
@@ -0,0 +1,80 @@
|
|
|
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, quoted for the shell it runs in", async () => {
|
|
27
|
+
const root = await makeWorkspace({ akanjs: "3.1.0-rc.6" });
|
|
28
|
+
const platform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
29
|
+
const warningOn = (os: NodeJS.Platform) => {
|
|
30
|
+
Object.defineProperty(process, "platform", { value: os });
|
|
31
|
+
return WorkspaceVersionCheck.warning("3.1.0", argv("sync", "demo", "--title", "it's"), root);
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
const posix = warningOn("linux");
|
|
35
|
+
expect(posix).toContain("@akanjs/cli 3.1.0, but this workspace has akanjs 3.1.0-rc.6");
|
|
36
|
+
expect(posix).toContain(`Run the matching CLI: bunx -p @akanjs/cli@3.1.0-rc.6 akan sync demo --title 'it'\\''s'`);
|
|
37
|
+
expect(posix).toEndWith("together: akan update");
|
|
38
|
+
expect(warningOn("win32")).toContain(
|
|
39
|
+
`Run the matching CLI: bunx -p @akanjs/cli@3.1.0-rc.6 akan sync demo --title "it's"`,
|
|
40
|
+
);
|
|
41
|
+
} finally {
|
|
42
|
+
if (platform) Object.defineProperty(process, "platform", platform);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("offers akan update with the CLI's prerelease tag only when the CLI is the newer one", async () => {
|
|
47
|
+
const older = await makeWorkspace({ akanjs: "3.1.1-rc.4" });
|
|
48
|
+
expect(WorkspaceVersionCheck.warning("3.1.1-rc.5", argv("build", "demo"), older)).toEndWith("akan update --tag rc");
|
|
49
|
+
|
|
50
|
+
const newer = await makeWorkspace({ akanjs: "3.1.1-rc.5" });
|
|
51
|
+
const warning = WorkspaceVersionCheck.warning("3.1.0", argv("build", "demo"), newer);
|
|
52
|
+
expect(warning).not.toContain("akan update");
|
|
53
|
+
expect(warning).toEndWith("Pin it so `bunx akan` runs it: bun add -d @akanjs/cli@3.1.1-rc.5");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("points at the workspace's own pinned CLI when it matches akanjs", async () => {
|
|
57
|
+
const root = await makeWorkspace({ akanjs: "3.1.1-rc.5", cli: "3.1.1-rc.5" });
|
|
58
|
+
|
|
59
|
+
const warning = WorkspaceVersionCheck.warning("3.1.0", argv("start", "demo"), root);
|
|
60
|
+
|
|
61
|
+
expect(warning).toContain("Run the matching CLI: bunx akan start demo");
|
|
62
|
+
expect(warning).not.toContain("bun add -d");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("leaves update, create-workspace, help and version alone", async () => {
|
|
66
|
+
const root = await makeWorkspace({ akanjs: "3.1.0-rc.6" });
|
|
67
|
+
for (const args of [["update"], ["create-workspace", "next"], ["--version"], ["-V"], ["sync", "--help"]])
|
|
68
|
+
expect(WorkspaceVersionCheck.warning("3.1.0", argv(...args), root)).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("skips a workspace that links akanjs from its own source", async () => {
|
|
72
|
+
const root = await makeWorkspace({});
|
|
73
|
+
await writeJson(path.join(root, "pkgs/akanjs/package.json"), { name: "akanjs", version: "3.1.1-rc.4" });
|
|
74
|
+
await mkdir(path.join(root, "node_modules"), { recursive: true });
|
|
75
|
+
await symlink(path.join(root, "pkgs/akanjs"), path.join(root, "node_modules/akanjs"), "dir");
|
|
76
|
+
|
|
77
|
+
expect(WorkspaceVersionCheck.installedAkanVersion(root)).toBeNull();
|
|
78
|
+
expect(WorkspaceVersionCheck.warning("3.1.0", argv("sync", "demo"), root)).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
if (/^[\w@%+=:,./-]+$/.test(arg)) return arg;
|
|
49
|
+
//? cmd and PowerShell take neither single quotes nor '\'' as POSIX shells do.
|
|
50
|
+
if (process.platform === "win32") return `"${arg.replaceAll('"', '\\"')}"`;
|
|
51
|
+
return `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -61,7 +61,12 @@ describe.skipIf(!CsrE2eHarness.enabled)("SSR dev registry (minimal)", () => {
|
|
|
61
61
|
let ssr: CsrE2eHarness;
|
|
62
62
|
|
|
63
63
|
beforeAll(async () => {
|
|
64
|
-
ssr = await CsrE2eHarness.start({
|
|
64
|
+
ssr = await CsrE2eHarness.start({
|
|
65
|
+
app: "minimal",
|
|
66
|
+
port,
|
|
67
|
+
// The server-log assertions match verbose-level lines, which a workspace `.env` may or may not turn on.
|
|
68
|
+
env: { AKAN_DEV_SSR_ARM_DELAY_MS: "3000", AKAN_PUBLIC_LOG_LEVEL: "verbose" },
|
|
69
|
+
});
|
|
65
70
|
}, 240_000);
|
|
66
71
|
|
|
67
72
|
afterAll(async () => {
|
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 [
|
|
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: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { AkanAppHost } from "../akanApp";
|
|
2
3
|
import { DevGeneratedIndexSync } from "../frontendBuild";
|
|
3
4
|
import { DevStabilityHarness, type DevStabilityHmrProbe } from "./devStabilityHarness";
|
|
4
5
|
|
|
@@ -467,7 +468,8 @@ export class FixtureService extends serve("fixture" as const, { serverMode: "bat
|
|
|
467
468
|
const harness = await createHarness();
|
|
468
469
|
const port = await harness.resolvePort();
|
|
469
470
|
// Child 0's deterministic ws port, held the way an orphaned replica from a killed run would hold it.
|
|
470
|
-
|
|
471
|
+
//? On the child's own 127.0.0.1: macOS lets a loopback bind share a port a wildcard listener holds.
|
|
472
|
+
const blocker = Bun.serve({ port: port + 10_000, hostname: "127.0.0.1", fetch: () => new Response("occupied") });
|
|
471
473
|
try {
|
|
472
474
|
const host = await harness.startHost();
|
|
473
475
|
await host.waitForLog(/falling back to an ephemeral port/);
|
|
@@ -583,9 +585,9 @@ describe("dev resource budgets", () => {
|
|
|
583
585
|
expect(await DevStabilityHarness.buildWorkerProcess(host.proc.pid)).toBeNull();
|
|
584
586
|
});
|
|
585
587
|
|
|
586
|
-
//? Between a fresh builder and one that has built a route, or the host stops enforcing it or never recycles.
|
|
587
|
-
//?
|
|
588
|
-
const CEILING_MB = process.platform === "
|
|
588
|
+
//? Between a fresh builder and one that has built a route, or the host stops enforcing it or never recycles. Linux
|
|
589
|
+
//? measured 101-115MiB fresh, 157-176MiB built and settled; macOS's footprint (Bun 1.4.4) 65MiB fresh, 109MiB built.
|
|
590
|
+
const CEILING_MB = process.platform === "darwin" ? 87 : 135;
|
|
589
591
|
|
|
590
592
|
budgetTest("recycles the builder at an unmeetable ceiling and keeps developing through it", async () => {
|
|
591
593
|
const harness = await createHarness();
|
|
@@ -635,9 +637,11 @@ describe("dev resource budgets", () => {
|
|
|
635
637
|
await host.waitForLogSince(mark, /pages-rebundle ok/, WAIT_MS).catch(() => undefined);
|
|
636
638
|
}
|
|
637
639
|
const settled = await DevStabilityHarness.builderProcess(host.proc.pid);
|
|
640
|
+
//? Read as the host reads it: on macOS `ps` also counts the pages the builder freed.
|
|
641
|
+
const settledRss = settled ? await AkanAppHost.readProcessRssBytes(settled.pid) : null;
|
|
638
642
|
const warned = /ceiling costs about one boot build per interval/.test(host.logs.join("").slice(start));
|
|
639
643
|
//? Past the interval (a slow machine) the next report recycles instead of warning: either answer settles it.
|
|
640
|
-
if ((
|
|
644
|
+
if ((settledRss ?? 0) >= CEILING_MB * MB && !warned)
|
|
641
645
|
await host.waitForLogSince(
|
|
642
646
|
settledFrom,
|
|
643
647
|
/ceiling costs about one boot build per interval|recycling builder pid=\d+|skipped: the builder fell to/,
|
package/lint/gritRules.test.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
-
//?
|
|
117
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
});
|