@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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { type FileHandle, open } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
//* An uncompressed POSIX tar of regular files, the same bytes on every OS. A system tar also writes what the host keeps
|
|
5
|
+
//* beside a file (macOS bsdtar an AppleDouble `._` entry for extended attributes), which a receiver refusing unlisted
|
|
6
|
+
//* entries rejects. A name past the ustar field's 100 bytes travels in a pax `path` record before its header.
|
|
7
|
+
export class TarWriter {
|
|
8
|
+
static readonly #block = 512;
|
|
9
|
+
static readonly #nameBytes = 100;
|
|
10
|
+
//? The size field holds 11 octal digits.
|
|
11
|
+
static readonly #maxSize = 8 ** 11 - 1;
|
|
12
|
+
readonly #handle: FileHandle;
|
|
13
|
+
readonly #mtime: number;
|
|
14
|
+
|
|
15
|
+
private constructor(handle: FileHandle, mtime: number) {
|
|
16
|
+
this.#handle = handle;
|
|
17
|
+
this.#mtime = mtime;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//? `mtime` (seconds) goes on every entry, so the same files make the same archive.
|
|
21
|
+
static async create(file: string, { mtime = 0 }: { mtime?: number } = {}) {
|
|
22
|
+
return new TarWriter(await open(file, "wx"), mtime);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async add(name: string, source: Blob): Promise<string> {
|
|
26
|
+
const { size } = source;
|
|
27
|
+
if (size > TarWriter.#maxSize) throw new Error(`${name} is ${size} bytes, more than a tar entry holds.`);
|
|
28
|
+
if (Buffer.byteLength(name) > TarWriter.#nameBytes) {
|
|
29
|
+
const pax = Buffer.from(TarWriter.#paxRecord("path", name));
|
|
30
|
+
await this.#write(
|
|
31
|
+
TarWriter.#header(`./PaxHeaders/${name.split("/").at(-1) ?? ""}`, pax.length, this.#mtime, "x"),
|
|
32
|
+
);
|
|
33
|
+
await this.#write(pax);
|
|
34
|
+
await this.#pad(pax.length);
|
|
35
|
+
}
|
|
36
|
+
await this.#write(TarWriter.#header(name, size, this.#mtime, "0"));
|
|
37
|
+
const hasher = createHash("sha256");
|
|
38
|
+
let written = 0;
|
|
39
|
+
for await (const chunk of source.stream()) {
|
|
40
|
+
hasher.update(chunk);
|
|
41
|
+
await this.#write(chunk);
|
|
42
|
+
written += chunk.length;
|
|
43
|
+
}
|
|
44
|
+
if (written !== size) throw new Error(`${name} changed while it was archived: ${written} bytes, not ${size}.`);
|
|
45
|
+
await this.#pad(size);
|
|
46
|
+
return hasher.digest("hex");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async close() {
|
|
50
|
+
await this.#write(new Uint8Array(TarWriter.#block * 2));
|
|
51
|
+
await this.#handle.close();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async discard() {
|
|
55
|
+
await this.#handle.close().catch(() => undefined);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async #write(bytes: Uint8Array) {
|
|
59
|
+
for (let at = 0; at < bytes.length; ) at += (await this.#handle.write(bytes, at)).bytesWritten;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async #pad(size: number) {
|
|
63
|
+
const rest = size % TarWriter.#block;
|
|
64
|
+
if (rest) await this.#write(new Uint8Array(TarWriter.#block - rest));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//? A record's length counts its own digits: `<length> <key>=<value>\n`.
|
|
68
|
+
static #paxRecord(key: string, value: string) {
|
|
69
|
+
const body = Buffer.byteLength(` ${key}=${value}\n`);
|
|
70
|
+
let length = body + 1;
|
|
71
|
+
while (body + String(length).length !== length) length = body + String(length).length;
|
|
72
|
+
return `${length} ${key}=${value}\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static #header(name: string, size: number, mtime: number, type: "0" | "x") {
|
|
76
|
+
const header = Buffer.alloc(TarWriter.#block);
|
|
77
|
+
header.write(name, 0, TarWriter.#nameBytes, "utf8");
|
|
78
|
+
const octal = (value: number, at: number, width: number) =>
|
|
79
|
+
header.write(`${value.toString(8).padStart(width - 1, "0")}\0`, at, width, "latin1");
|
|
80
|
+
octal(0o644, 100, 8);
|
|
81
|
+
octal(0, 108, 8);
|
|
82
|
+
octal(0, 116, 8);
|
|
83
|
+
octal(size, 124, 12);
|
|
84
|
+
octal(mtime, 136, 12);
|
|
85
|
+
header.write(" ", 148, 8, "latin1");
|
|
86
|
+
header.write(type, 156, 1, "latin1");
|
|
87
|
+
header.write("ustar\0", 257, 6, "latin1");
|
|
88
|
+
header.write("00", 263, 2, "latin1");
|
|
89
|
+
const checksum = header.reduce((sum, byte) => sum + byte, 0);
|
|
90
|
+
header.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, 8, "latin1");
|
|
91
|
+
return header;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -83,6 +83,40 @@ describe("UpdateUploader", () => {
|
|
|
83
83
|
);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
test("uploads a desktop release's envelope, with the v1 pair only when the pair is that release", async () => {
|
|
87
|
+
const manifest = { kind: "app", bundle: "1.3.0-3000", archive: { url: `app/${sha("c")}.tar.gz` }, patches: [] };
|
|
88
|
+
await inUpdatesDir(
|
|
89
|
+
{
|
|
90
|
+
[`macos-arm64/app/${sha("c")}.tar.gz`]: "tar",
|
|
91
|
+
"macos-arm64/main/app.json": envelope(manifest),
|
|
92
|
+
"macos-arm64/main.json": JSON.stringify({ ...manifest, bundle: "1.2.0-2000" }),
|
|
93
|
+
"macos-arm64/main.json.sig": "sig",
|
|
94
|
+
"macos-arm64/beta/app.json": envelope({ ...manifest, bundle: "1.4.0-4000" }),
|
|
95
|
+
"macos-arm64/beta.json": JSON.stringify({ ...manifest, bundle: "1.4.0-4000" }),
|
|
96
|
+
"macos-arm64/beta.json.sig": "sig",
|
|
97
|
+
},
|
|
98
|
+
async (updatesDir) => {
|
|
99
|
+
const dir = path.join(updatesDir, "macos-arm64");
|
|
100
|
+
const upload = async (channel: string, bundle: string) => {
|
|
101
|
+
const { bucket, writes } = fakeBucket();
|
|
102
|
+
await new UpdateUploader(bucket, "").upload(updatesDir, { dir, channel, bundle });
|
|
103
|
+
return writes.map(({ key }) => key);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
expect(await upload("main", "1.3.0-3000")).toEqual([
|
|
107
|
+
`macos-arm64/app/${sha("c")}.tar.gz`,
|
|
108
|
+
"macos-arm64/main/app.json",
|
|
109
|
+
]);
|
|
110
|
+
expect(await upload("beta", "1.4.0-4000")).toEqual([
|
|
111
|
+
`macos-arm64/app/${sha("c")}.tar.gz`,
|
|
112
|
+
"macos-arm64/beta.json.sig",
|
|
113
|
+
"macos-arm64/beta.json",
|
|
114
|
+
"macos-arm64/beta/app.json",
|
|
115
|
+
]);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
86
120
|
test("uploads a phone release's own files and envelope, never pack records or another manifest beside them", async () => {
|
|
87
121
|
await inUpdatesDir(
|
|
88
122
|
{
|
package/mobile/updateUploader.ts
CHANGED
|
@@ -51,12 +51,8 @@ export class UpdateUploader {
|
|
|
51
51
|
//* APIs, which a later publish from another machine may have replaced.
|
|
52
52
|
static async releaseFiles(updatesDir: string, { dir, channel, bundle }: PublishedUpdate): Promise<string[]> {
|
|
53
53
|
const keyOf = (file: unknown) => UpdateUploader.#keyOf(updatesDir, dir, file);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const archive = desktop.archive?.url;
|
|
57
|
-
const patches = UpdateUploader.#field(desktop.patches, "url");
|
|
58
|
-
return [archive, ...patches, `${channel}.json.sig`, `${channel}.json`].map(keyOf);
|
|
59
|
-
}
|
|
54
|
+
const v1 = await UpdateUploader.#read<UpdateManifest>(path.join(dir, `${channel}.json`));
|
|
55
|
+
const pair = v1?.bundle === bundle ? [`${channel}.json.sig`, `${channel}.json`] : [];
|
|
60
56
|
const names = await readdir(path.join(dir, channel)).catch(() => [] as string[]);
|
|
61
57
|
for (const name of names.filter((name) => name.endsWith(".json"))) {
|
|
62
58
|
const envelope = await UpdateUploader.#read<UpdateEnvelope>(path.join(dir, channel, name));
|
|
@@ -65,6 +61,8 @@ export class UpdateUploader {
|
|
|
65
61
|
? UpdateUploader.#parse<UpdateManifest>(Buffer.from(envelope.manifest, "base64").toString("utf8"))
|
|
66
62
|
: null;
|
|
67
63
|
if (manifest?.bundle !== bundle) continue;
|
|
64
|
+
if (manifest.kind === "app")
|
|
65
|
+
return [...UpdateUploader.#appFiles(manifest), ...pair, `${channel}/${name}`].map(keyOf);
|
|
68
66
|
const hashes = [...new Set(UpdateUploader.#field(manifest.files, "sha256"))];
|
|
69
67
|
if (!hashes.every((hash): hash is string => typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash)))
|
|
70
68
|
throw new Error(
|
|
@@ -72,9 +70,14 @@ export class UpdateUploader {
|
|
|
72
70
|
);
|
|
73
71
|
return [...hashes.map((hash) => `${UpdatePackFolder.filesDir}/${hash}`), `${channel}/${name}`].map(keyOf);
|
|
74
72
|
}
|
|
73
|
+
if (v1 && pair.length) return [...UpdateUploader.#appFiles(v1), ...pair].map(keyOf);
|
|
75
74
|
throw new Error(`${dir} holds no ${channel} manifest of release ${bundle}; nothing was uploaded.`);
|
|
76
75
|
}
|
|
77
76
|
|
|
77
|
+
static #appFiles(manifest: Partial<UpdateManifest>): unknown[] {
|
|
78
|
+
return [manifest.archive?.url, ...UpdateUploader.#field(manifest.patches, "url")];
|
|
79
|
+
}
|
|
80
|
+
|
|
78
81
|
static #keyOf(updatesDir: string, dir: string, file: unknown) {
|
|
79
82
|
const resolved = typeof file === "string" ? path.resolve(dir, file) : "";
|
|
80
83
|
const key = path.relative(updatesDir, resolved);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.1.1-rc.
|
|
3
|
+
"version": "3.1.1-rc.11",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@earendil-works/pi-tui": "1.3.0",
|
|
49
49
|
"@inquirer/prompts": "^8.7.3",
|
|
50
50
|
"@tailwindcss/node": "^4.3.3",
|
|
51
|
-
"akanjs": "3.1.1-rc.
|
|
51
|
+
"akanjs": "3.1.1-rc.11",
|
|
52
52
|
"chalk": "^6.0.1",
|
|
53
53
|
"commander": "^15.0.0",
|
|
54
54
|
"dayjs": "^1.11.23",
|
|
@@ -40,6 +40,9 @@ export class LinuxDockerTarget extends PlatformTestTarget {
|
|
|
40
40
|
"run",
|
|
41
41
|
"-d",
|
|
42
42
|
"--rm",
|
|
43
|
+
//? An init as PID 1 reaps orphans as a real host does; under `sleep` a killed orphan stays a zombie that still
|
|
44
|
+
//? answers kill(pid, 0).
|
|
45
|
+
"--init",
|
|
43
46
|
"--name",
|
|
44
47
|
container,
|
|
45
48
|
"--cpus",
|
|
@@ -110,6 +110,21 @@ export class ItemEndpoint extends endpoint(srv.item, ({ query, mutation }) => ({
|
|
|
110
110
|
expect(index.isMutation("apps/third/page/x.tsx", "approveTicket")).toBe(false);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
test("reads a lib page's call by its own lib's endpoint before another lib's of that name", () => {
|
|
114
|
+
const shared = fileOf(
|
|
115
|
+
"libs/shared/lib/_shared/shared.signal.ts",
|
|
116
|
+
`export class SharedEndpoint extends endpoint(srv.shared, ({ mutation }) => ({ syncFeed: mutation(Boolean, {}).exec(() => true) })) {}`,
|
|
117
|
+
);
|
|
118
|
+
const social = fileOf(
|
|
119
|
+
"libs/social/lib/_social/social.signal.ts",
|
|
120
|
+
`export class SocialEndpoint extends endpoint(srv.social, ({ query }) => ({ syncFeed: query(Boolean, {}).exec(() => true) })) {}`,
|
|
121
|
+
);
|
|
122
|
+
const index = new SignalMutationIndex([shared, social]);
|
|
123
|
+
expect(index.isMutation("libs/social/page/x.tsx", "syncFeed")).toBe(false);
|
|
124
|
+
expect(index.policyIndexOf("libs/social/page/x.tsx", "syncFeed")).toBeNull();
|
|
125
|
+
expect(index.isMutation("libs/shared/page/x.tsx", "syncFeed")).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
113
128
|
test("ignores signal tests and specs", () => {
|
|
114
129
|
const spec = fileOf(
|
|
115
130
|
"apps/demo/lib/ticket/ticket.signal.spec.ts",
|
package/signalMutationIndex.ts
CHANGED
|
@@ -34,7 +34,7 @@ export class SignalMutationIndex {
|
|
|
34
34
|
|
|
35
35
|
#shapeOf(consumerFile: string, name: string) {
|
|
36
36
|
const [root, owner] = consumerFile.split("/");
|
|
37
|
-
const own =
|
|
37
|
+
const own = this.#shapesByOwner.get(`${root}/${owner}`)?.get(name);
|
|
38
38
|
if (own) return own;
|
|
39
39
|
for (const [key, shapes] of this.#shapesByOwner) {
|
|
40
40
|
const shape = shapes.get(name);
|
package/types.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface PackageJson {
|
|
|
12
12
|
optionalDependencies?: Record<string, string>;
|
|
13
13
|
trustedDependencies?: string[];
|
|
14
14
|
overrides?: Record<string, string>;
|
|
15
|
+
resolutions?: Record<string, string>;
|
|
15
16
|
engines?: Record<string, string>;
|
|
16
17
|
exports?: Record<string, string | Record<string, string>>;
|
|
17
18
|
bun?: {
|