Repository navigation
Expand file tree
/
Copy pathIdentity.ts
More file actions
124 lines (113 loc) · 4.31 KB
/
Copy pathIdentity.ts
File metadata and controls
124 lines (113 loc) · 4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { Crypto, Effect, FileSystem, Path, Schema } from "effect";
import type { PlatformError } from "effect/PlatformError";
import { InvalidProjectRootError, InvalidStackIdentityError } from "./Errors.ts";
import { StackIdSchema } from "./StackId.ts";
import { resolveGitBranchContext } from "./GitBranchContext.ts";
const DEFAULT_STACK_NAME = "default";
export interface StackIdentity {
readonly projectRoot: string;
readonly branchContext: string;
readonly stackName: string;
}
export interface ResolveStackIdentityOptions {
readonly projectRoot: string;
readonly name?: string;
}
const identityFailure = (message: string, fields?: Readonly<Record<string, unknown>>) =>
new InvalidStackIdentityError({ message, ...fields });
const encodeTuple = (identity: StackIdentity): Uint8Array => {
const encoder = new TextEncoder();
const fields = [identity.projectRoot, identity.branchContext, identity.stackName];
const encoded = fields.map((field) => encoder.encode(field));
const byteLength = encoded.reduce((total, bytes) => total + 4 + bytes.byteLength, 0);
const result = new Uint8Array(byteLength);
const view = new DataView(result.buffer);
let offset = 0;
for (const bytes of encoded) {
view.setUint32(offset, bytes.byteLength, false);
offset += 4;
result.set(bytes, offset);
offset += bytes.byteLength;
}
return result;
};
/** Hashes the complete identity tuple using length-delimited UTF-8 fields. */
export const deriveStackId = Effect.fn("Identity.deriveStackId")(function* (
identity: StackIdentity,
) {
return yield* Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const digest = yield* crypto.digest("SHA-256", encodeTuple(identity));
let hexadecimal = "";
for (const byte of digest) {
hexadecimal += byte.toString(16).padStart(2, "0");
}
return yield* Schema.decodeEffect(StackIdSchema)(hexadecimal).pipe(
Effect.mapError(
(error) =>
new InvalidStackIdentityError({
message: `SHA-256 digest did not produce a valid StackId: ${String(error)}`,
}),
),
);
}).pipe(
Effect.catchTag("PlatformError", (error: PlatformError) =>
Effect.fail(
new InvalidStackIdentityError({
message: `Unable to derive StackId: ${error.message}`,
}),
),
),
);
});
/** Resolves a canonical Git or ordinary-folder identity without writing state. */
export const resolveStackIdentity = Effect.fn("Identity.resolveStackIdentity")(function* (
options: ResolveStackIdentityOptions,
) {
return yield* Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const projectRoot = options.projectRoot;
if (projectRoot.trim().length === 0) {
return yield* new InvalidProjectRootError({
projectRoot: options.projectRoot,
message: "The project root must not be blank",
});
}
const canonicalProjectRoot = yield* fs.realPath(projectRoot);
const info = yield* fs.stat(canonicalProjectRoot);
if (info.type !== "Directory") {
return yield* new InvalidProjectRootError({
projectRoot,
message: "The project root is not a directory",
});
}
const stackName = options.name ?? DEFAULT_STACK_NAME;
if (stackName.trim().length === 0) {
return yield* identityFailure("The stack name must not be blank", { name: options.name });
}
const branchContext = yield* resolveGitBranchContext(canonicalProjectRoot);
return {
projectRoot: canonicalProjectRoot,
branchContext: branchContext ?? "ordinary-workspace",
stackName,
};
}).pipe(
Effect.catchTag("PlatformError", (error: PlatformError) =>
Effect.fail(
new InvalidProjectRootError({
projectRoot: options.projectRoot,
message: `Unable to inspect the project root: ${error.message}`,
}),
),
),
);
});
/**
* Names a stack for container names and grouping labels: the project root's folder name plus any
* non-default stack name, or the stack name alone when the root has no folder name.
*/
export const projectSegmentFor = (identity: StackIdentity, path: Path.Path): string => {
const base = path.basename(identity.projectRoot);
if (base.trim().length === 0) return identity.stackName;
return identity.stackName === DEFAULT_STACK_NAME ? base : `${base}-${identity.stackName}`;
};