@zodmire/core 0.1.0
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/LICENSE +21 -0
- package/artifacts-CUzHfc15.mjs +91 -0
- package/artifacts-QqCpfT50.d.mts +25 -0
- package/materialize.d.mts +68 -0
- package/materialize.mjs +240 -0
- package/mod.d.mts +812 -0
- package/mod.mjs +6878 -0
- package/package.json +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Vercel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//#region packages/core/artifacts.ts
|
|
2
|
+
function safeDataName(value) {
|
|
3
|
+
return value.replace(/[\\/]+/g, "__").replace(/[^A-Za-z0-9_.-]+/g, "_");
|
|
4
|
+
}
|
|
5
|
+
function createGeneratedArtifact(logicalPath, content, ownership) {
|
|
6
|
+
return {
|
|
7
|
+
logicalPath,
|
|
8
|
+
dataName: safeDataName(logicalPath),
|
|
9
|
+
content,
|
|
10
|
+
owned: true,
|
|
11
|
+
ownershipScope: ownership?.ownershipScope,
|
|
12
|
+
scopeKey: ownership?.scopeKey
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function withArtifactOwnership(artifact, ownership) {
|
|
16
|
+
return {
|
|
17
|
+
...artifact,
|
|
18
|
+
ownershipScope: ownership.ownershipScope,
|
|
19
|
+
scopeKey: ownership.scopeKey
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function mergeGeneratedArtifacts(...groups) {
|
|
23
|
+
const mergedArtifacts = [];
|
|
24
|
+
const artifactsByLogicalPath = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const artifact of groups.flat()) {
|
|
26
|
+
const existingArtifact = artifactsByLogicalPath.get(artifact.logicalPath);
|
|
27
|
+
if (existingArtifact === void 0) {
|
|
28
|
+
artifactsByLogicalPath.set(artifact.logicalPath, artifact);
|
|
29
|
+
mergedArtifacts.push(artifact);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (existingArtifact.dataName !== artifact.dataName || existingArtifact.content !== artifact.content || existingArtifact.ownershipScope !== artifact.ownershipScope || existingArtifact.scopeKey !== artifact.scopeKey) throw new Error(`Generated artifact logical path collision: ${artifact.logicalPath}`);
|
|
33
|
+
}
|
|
34
|
+
return mergedArtifacts;
|
|
35
|
+
}
|
|
36
|
+
function sharedArtifactOwnership(scopeKey = "shared") {
|
|
37
|
+
return {
|
|
38
|
+
ownershipScope: "shared-owned",
|
|
39
|
+
scopeKey
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function sliceArtifactOwnership(scopeKey) {
|
|
43
|
+
return {
|
|
44
|
+
ownershipScope: "slice-owned",
|
|
45
|
+
scopeKey
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function contextArtifactOwnership(scopeKey) {
|
|
49
|
+
return {
|
|
50
|
+
ownershipScope: "context-owned",
|
|
51
|
+
scopeKey
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function applyOwnershipIfMissing(artifacts, ownership) {
|
|
55
|
+
return artifacts.map((artifact) => artifact.ownershipScope !== void 0 && artifact.scopeKey !== void 0 ? artifact : withArtifactOwnership(artifact, ownership));
|
|
56
|
+
}
|
|
57
|
+
function inferArtifactOwnership(logicalPath, defaultSliceScopeKey = "default") {
|
|
58
|
+
if (logicalPath.startsWith("lib/")) return sharedArtifactOwnership("lib");
|
|
59
|
+
if (logicalPath.startsWith("infrastructure/di/")) return sharedArtifactOwnership("composition");
|
|
60
|
+
if (logicalPath.startsWith("presentation/event-handlers/")) return sharedArtifactOwnership("composition");
|
|
61
|
+
if (logicalPath === "presentation/trpc/router.ts" || logicalPath === "presentation/trpc/context.ts") return sharedArtifactOwnership("composition");
|
|
62
|
+
if (logicalPath.startsWith("core/ports/messaging/")) return sharedArtifactOwnership("composition");
|
|
63
|
+
if (logicalPath.startsWith("infrastructure/unit-of-work/")) return sharedArtifactOwnership("composition");
|
|
64
|
+
if (logicalPath.startsWith("infrastructure/outbox/")) return sharedArtifactOwnership("composition");
|
|
65
|
+
if (logicalPath.startsWith("core/shared-kernel/") || logicalPath.startsWith("core/ports/")) return sharedArtifactOwnership();
|
|
66
|
+
const contextMatch = logicalPath.match(/^core\/contexts\/(.+?)\/(?:application|domain|tests)\//);
|
|
67
|
+
if (contextMatch) return sliceArtifactOwnership(contextMatch[1] ?? defaultSliceScopeKey);
|
|
68
|
+
if (logicalPath.match(/^infrastructure\/persistence\/[^/]+\/tables\.ts$/)) {
|
|
69
|
+
const ctx = logicalPath.split("/")[2];
|
|
70
|
+
return sliceArtifactOwnership(ctx ?? defaultSliceScopeKey);
|
|
71
|
+
}
|
|
72
|
+
if (logicalPath.match(/^infrastructure\/view-models\/[^/]+\/tables\.ts$/)) {
|
|
73
|
+
const ctx = logicalPath.split("/")[2];
|
|
74
|
+
return sliceArtifactOwnership(ctx ?? defaultSliceScopeKey);
|
|
75
|
+
}
|
|
76
|
+
const repositoryMatch = logicalPath.match(/^infrastructure\/.*\/repositories\/(.+)\/[^/]+$/);
|
|
77
|
+
if (repositoryMatch) return sliceArtifactOwnership(repositoryMatch[1] ?? defaultSliceScopeKey);
|
|
78
|
+
const routerMatch = logicalPath.match(/^presentation\/.*\/routers\/([^.]+)\.router\.ts$/);
|
|
79
|
+
if (routerMatch) return sliceArtifactOwnership(routerMatch[1] ?? defaultSliceScopeKey);
|
|
80
|
+
return sliceArtifactOwnership(defaultSliceScopeKey);
|
|
81
|
+
}
|
|
82
|
+
function resolveArtifactOwnership(artifact, defaultSliceScopeKey = "default") {
|
|
83
|
+
if (artifact.ownershipScope && artifact.scopeKey) return {
|
|
84
|
+
ownershipScope: artifact.ownershipScope,
|
|
85
|
+
scopeKey: artifact.scopeKey
|
|
86
|
+
};
|
|
87
|
+
return inferArtifactOwnership(artifact.logicalPath, defaultSliceScopeKey);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
export { mergeGeneratedArtifacts as a, sliceArtifactOwnership as c, inferArtifactOwnership as i, withArtifactOwnership as l, contextArtifactOwnership as n, resolveArtifactOwnership as o, createGeneratedArtifact as r, sharedArtifactOwnership as s, applyOwnershipIfMissing as t };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region packages/core/artifacts.d.ts
|
|
2
|
+
type OwnershipScope = "shared-owned" | "slice-owned" | "context-owned";
|
|
3
|
+
type ArtifactOwnership = {
|
|
4
|
+
ownershipScope: OwnershipScope;
|
|
5
|
+
scopeKey: string;
|
|
6
|
+
};
|
|
7
|
+
type GeneratedArtifact = {
|
|
8
|
+
logicalPath: string;
|
|
9
|
+
dataName: string;
|
|
10
|
+
content: string;
|
|
11
|
+
owned: true;
|
|
12
|
+
ownershipScope?: OwnershipScope;
|
|
13
|
+
scopeKey?: string;
|
|
14
|
+
};
|
|
15
|
+
declare function createGeneratedArtifact(logicalPath: string, content: string, ownership?: ArtifactOwnership): GeneratedArtifact;
|
|
16
|
+
declare function withArtifactOwnership(artifact: GeneratedArtifact, ownership: ArtifactOwnership): GeneratedArtifact;
|
|
17
|
+
declare function mergeGeneratedArtifacts(...groups: GeneratedArtifact[][]): GeneratedArtifact[];
|
|
18
|
+
declare function sharedArtifactOwnership(scopeKey?: string): ArtifactOwnership;
|
|
19
|
+
declare function sliceArtifactOwnership(scopeKey: string): ArtifactOwnership;
|
|
20
|
+
declare function contextArtifactOwnership(scopeKey: string): ArtifactOwnership;
|
|
21
|
+
declare function applyOwnershipIfMissing(artifacts: GeneratedArtifact[], ownership: ArtifactOwnership): GeneratedArtifact[];
|
|
22
|
+
declare function inferArtifactOwnership(logicalPath: string, defaultSliceScopeKey?: string): ArtifactOwnership;
|
|
23
|
+
declare function resolveArtifactOwnership(artifact: Pick<GeneratedArtifact, "logicalPath" | "ownershipScope" | "scopeKey">, defaultSliceScopeKey?: string): ArtifactOwnership;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { contextArtifactOwnership as a, mergeGeneratedArtifacts as c, sliceArtifactOwnership as d, withArtifactOwnership as f, applyOwnershipIfMissing as i, resolveArtifactOwnership as l, GeneratedArtifact as n, createGeneratedArtifact as o, OwnershipScope as r, inferArtifactOwnership as s, ArtifactOwnership as t, sharedArtifactOwnership as u };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { n as GeneratedArtifact } from "./artifacts-QqCpfT50.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region packages/core/materialize.d.ts
|
|
5
|
+
declare const OwnedManifestEntrySchema: z.ZodObject<{
|
|
6
|
+
logicalPath: z.ZodString;
|
|
7
|
+
ownershipScope: z.ZodEnum<{
|
|
8
|
+
"shared-owned": "shared-owned";
|
|
9
|
+
"slice-owned": "slice-owned";
|
|
10
|
+
"context-owned": "context-owned";
|
|
11
|
+
}>;
|
|
12
|
+
scopeKey: z.ZodString;
|
|
13
|
+
sourceArtifactId: z.ZodString;
|
|
14
|
+
contentSha256: z.ZodString;
|
|
15
|
+
generatorVersion: z.ZodString;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
declare const MaterializationManifestSchema: z.ZodObject<{
|
|
18
|
+
targetRoot: z.ZodString;
|
|
19
|
+
files: z.ZodArray<z.ZodObject<{
|
|
20
|
+
logicalPath: z.ZodString;
|
|
21
|
+
ownershipScope: z.ZodEnum<{
|
|
22
|
+
"shared-owned": "shared-owned";
|
|
23
|
+
"slice-owned": "slice-owned";
|
|
24
|
+
"context-owned": "context-owned";
|
|
25
|
+
}>;
|
|
26
|
+
scopeKey: z.ZodString;
|
|
27
|
+
sourceArtifactId: z.ZodString;
|
|
28
|
+
contentSha256: z.ZodString;
|
|
29
|
+
generatorVersion: z.ZodString;
|
|
30
|
+
}, z.core.$strict>>;
|
|
31
|
+
}, z.core.$strict>;
|
|
32
|
+
declare const LegacyMaterializationManifestSchema: z.ZodObject<{
|
|
33
|
+
targetRoot: z.ZodString;
|
|
34
|
+
files: z.ZodArray<z.ZodObject<{
|
|
35
|
+
logicalPath: z.ZodString;
|
|
36
|
+
owned: z.ZodLiteral<true>;
|
|
37
|
+
sourceArtifactId: z.ZodString;
|
|
38
|
+
contentSha256: z.ZodString;
|
|
39
|
+
}, z.core.$strict>>;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
type LegacyMaterializationManifest = z.output<typeof LegacyMaterializationManifestSchema>;
|
|
42
|
+
type OwnedManifestEntry = z.output<typeof OwnedManifestEntrySchema>;
|
|
43
|
+
type MaterializationManifest = z.output<typeof MaterializationManifestSchema>;
|
|
44
|
+
type MaterializationAction = "write" | "overwrite" | "conflict";
|
|
45
|
+
type MaterializedFile = {
|
|
46
|
+
logicalPath: string;
|
|
47
|
+
action: Exclude<MaterializationAction, "conflict"> | "delete";
|
|
48
|
+
};
|
|
49
|
+
type MaterializeResult = {
|
|
50
|
+
targetRoot: string;
|
|
51
|
+
files: MaterializedFile[];
|
|
52
|
+
};
|
|
53
|
+
type MaterializationScope = Pick<OwnedManifestEntry, "ownershipScope" | "scopeKey">;
|
|
54
|
+
type MaterializeOptions = {
|
|
55
|
+
reconcileScopes?: MaterializationScope[];
|
|
56
|
+
};
|
|
57
|
+
type PreparedArtifact = {
|
|
58
|
+
artifact: GeneratedArtifact;
|
|
59
|
+
content: string;
|
|
60
|
+
};
|
|
61
|
+
declare function determineMaterializationAction(destinationExists: boolean, previousManifestSha256?: string, currentDestinationSha256?: string): MaterializationAction;
|
|
62
|
+
declare function sha256Hex(content: string): Promise<string>;
|
|
63
|
+
declare function buildMaterializationManifest(targetRoot: string, artifacts: GeneratedArtifact[] | PreparedArtifact[], generatorVersion: string): Promise<MaterializationManifest>;
|
|
64
|
+
declare function upgradeLegacyMaterializationManifest(legacyManifest: LegacyMaterializationManifest): MaterializationManifest;
|
|
65
|
+
declare function getPersistedMaterializationManifestPath(targetRoot: string): string;
|
|
66
|
+
declare function materializeOwnedArtifacts(targetRoot: string, artifacts: GeneratedArtifact[], generatorVersion: string, options?: MaterializeOptions): Promise<MaterializeResult>;
|
|
67
|
+
//#endregion
|
|
68
|
+
export { LegacyMaterializationManifest, MaterializationAction, MaterializationManifest, MaterializationManifestSchema, MaterializationScope, MaterializeOptions, MaterializeResult, MaterializedFile, OwnedManifestEntry, OwnedManifestEntrySchema, buildMaterializationManifest, determineMaterializationAction, getPersistedMaterializationManifestPath, materializeOwnedArtifacts, sha256Hex, upgradeLegacyMaterializationManifest };
|
package/materialize.mjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { c as sliceArtifactOwnership, i as inferArtifactOwnership, o as resolveArtifactOwnership } from "./artifacts-CUzHfc15.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
//#region packages/core/materialize.ts
|
|
7
|
+
let _oxfmt;
|
|
8
|
+
async function loadOxfmt() {
|
|
9
|
+
if (!_oxfmt) _oxfmt = await import("oxfmt");
|
|
10
|
+
return _oxfmt;
|
|
11
|
+
}
|
|
12
|
+
const OwnershipScopeSchema = z.enum([
|
|
13
|
+
"shared-owned",
|
|
14
|
+
"slice-owned",
|
|
15
|
+
"context-owned"
|
|
16
|
+
]);
|
|
17
|
+
const OwnedManifestEntrySchema = z.strictObject({
|
|
18
|
+
logicalPath: z.string().min(1),
|
|
19
|
+
ownershipScope: OwnershipScopeSchema,
|
|
20
|
+
scopeKey: z.string().min(1),
|
|
21
|
+
sourceArtifactId: z.string().min(1),
|
|
22
|
+
contentSha256: z.string().length(64),
|
|
23
|
+
generatorVersion: z.string().min(1)
|
|
24
|
+
});
|
|
25
|
+
const MaterializationManifestSchema = z.strictObject({
|
|
26
|
+
targetRoot: z.string().min(1),
|
|
27
|
+
files: z.array(OwnedManifestEntrySchema)
|
|
28
|
+
});
|
|
29
|
+
const LegacyMaterializationManifestFileSchema = z.strictObject({
|
|
30
|
+
logicalPath: z.string().min(1),
|
|
31
|
+
owned: z.literal(true),
|
|
32
|
+
sourceArtifactId: z.string().min(1),
|
|
33
|
+
contentSha256: z.string().length(64)
|
|
34
|
+
});
|
|
35
|
+
const LegacyMaterializationManifestSchema = z.strictObject({
|
|
36
|
+
targetRoot: z.string().min(1),
|
|
37
|
+
files: z.array(LegacyMaterializationManifestFileSchema)
|
|
38
|
+
});
|
|
39
|
+
const persistedMaterializationManifestRelativePath = ".bounded-context-codegen/materialization-manifest.json";
|
|
40
|
+
function determineMaterializationAction(destinationExists, previousManifestSha256, currentDestinationSha256) {
|
|
41
|
+
if (!destinationExists) return "write";
|
|
42
|
+
if (!previousManifestSha256 || !currentDestinationSha256) return "conflict";
|
|
43
|
+
return currentDestinationSha256 === previousManifestSha256 ? "overwrite" : "conflict";
|
|
44
|
+
}
|
|
45
|
+
async function sha256Hex(content) {
|
|
46
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
|
|
47
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
48
|
+
}
|
|
49
|
+
async function buildMaterializationManifest(targetRoot, artifacts, generatorVersion) {
|
|
50
|
+
return {
|
|
51
|
+
targetRoot,
|
|
52
|
+
files: await Promise.all(artifacts.map(async (entry) => {
|
|
53
|
+
const artifact = "artifact" in entry ? entry.artifact : entry;
|
|
54
|
+
const content = "content" in entry ? entry.content : await prepareArtifactContent(artifact);
|
|
55
|
+
const ownership = resolveArtifactOwnership(artifact);
|
|
56
|
+
return {
|
|
57
|
+
logicalPath: artifact.logicalPath,
|
|
58
|
+
ownershipScope: ownership.ownershipScope,
|
|
59
|
+
scopeKey: ownership.scopeKey,
|
|
60
|
+
sourceArtifactId: artifact.dataName,
|
|
61
|
+
contentSha256: await sha256Hex(content),
|
|
62
|
+
generatorVersion
|
|
63
|
+
};
|
|
64
|
+
}))
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function inferLegacyManifestSliceScopeKey(legacyManifest) {
|
|
68
|
+
const unresolvedScopeKey = "__legacy-unresolved__";
|
|
69
|
+
const inferredScopeKeys = new Set(legacyManifest.files.flatMap((file) => {
|
|
70
|
+
if (!file.logicalPath.startsWith("core/contexts/") && !file.logicalPath.includes("/repositories/")) return [];
|
|
71
|
+
const ownership = inferArtifactOwnership(file.logicalPath, unresolvedScopeKey);
|
|
72
|
+
return ownership.ownershipScope === "slice-owned" && ownership.scopeKey !== unresolvedScopeKey ? [ownership.scopeKey] : [];
|
|
73
|
+
}));
|
|
74
|
+
return inferredScopeKeys.size === 1 ? [...inferredScopeKeys][0] : void 0;
|
|
75
|
+
}
|
|
76
|
+
function upgradeLegacyMaterializationManifest(legacyManifest) {
|
|
77
|
+
const defaultSliceScopeKey = inferLegacyManifestSliceScopeKey(legacyManifest) ?? "default";
|
|
78
|
+
return {
|
|
79
|
+
targetRoot: legacyManifest.targetRoot,
|
|
80
|
+
files: legacyManifest.files.map((file) => {
|
|
81
|
+
const ownership = file.logicalPath.startsWith("presentation/") || file.logicalPath.startsWith("routes/") ? sliceArtifactOwnership(defaultSliceScopeKey) : resolveArtifactOwnership(file, defaultSliceScopeKey);
|
|
82
|
+
return {
|
|
83
|
+
logicalPath: file.logicalPath,
|
|
84
|
+
ownershipScope: ownership.ownershipScope,
|
|
85
|
+
scopeKey: ownership.scopeKey,
|
|
86
|
+
sourceArtifactId: file.sourceArtifactId,
|
|
87
|
+
contentSha256: file.contentSha256,
|
|
88
|
+
generatorVersion: "legacy-v1"
|
|
89
|
+
};
|
|
90
|
+
})
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function getPersistedMaterializationManifestPath(targetRoot) {
|
|
94
|
+
return join(targetRoot, persistedMaterializationManifestRelativePath);
|
|
95
|
+
}
|
|
96
|
+
function ownershipScopeKey(entry) {
|
|
97
|
+
return `${entry.ownershipScope}:${entry.scopeKey}`;
|
|
98
|
+
}
|
|
99
|
+
function scopedLogicalPathKey(entry) {
|
|
100
|
+
return `${ownershipScopeKey(entry)}:${entry.logicalPath}`;
|
|
101
|
+
}
|
|
102
|
+
function findScopedManifestEntry(entries, currentEntry) {
|
|
103
|
+
const currentOwnershipScopeKey = ownershipScopeKey(currentEntry);
|
|
104
|
+
return entries.find((entry) => ownershipScopeKey(entry) === currentOwnershipScopeKey && (entry.sourceArtifactId === currentEntry.sourceArtifactId || entry.logicalPath === currentEntry.logicalPath));
|
|
105
|
+
}
|
|
106
|
+
async function readDestinationSha256(destinationPath) {
|
|
107
|
+
try {
|
|
108
|
+
return await sha256Hex(await readFile(destinationPath, "utf-8"));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code === "ENOENT") return;
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function readPersistedMaterializationManifest(targetRoot) {
|
|
115
|
+
try {
|
|
116
|
+
const persistedManifestText = await readFile(getPersistedMaterializationManifestPath(targetRoot), "utf-8");
|
|
117
|
+
const persistedManifest = JSON.parse(persistedManifestText);
|
|
118
|
+
const v2Manifest = MaterializationManifestSchema.safeParse(persistedManifest);
|
|
119
|
+
if (v2Manifest.success) return {
|
|
120
|
+
kind: "v2",
|
|
121
|
+
manifest: v2Manifest.data
|
|
122
|
+
};
|
|
123
|
+
const legacyManifest = LegacyMaterializationManifestSchema.safeParse(persistedManifest);
|
|
124
|
+
if (legacyManifest.success) return {
|
|
125
|
+
kind: "legacy-v1",
|
|
126
|
+
manifest: legacyManifest.data,
|
|
127
|
+
upgradedManifest: upgradeLegacyMaterializationManifest(legacyManifest.data)
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
kind: "v2",
|
|
131
|
+
manifest: MaterializationManifestSchema.parse(persistedManifest)
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error.code === "ENOENT") return { kind: "missing" };
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function indexLegacyManifestEntriesByLogicalPath(manifest) {
|
|
139
|
+
const entries = /* @__PURE__ */ new Map();
|
|
140
|
+
for (const file of manifest.files) {
|
|
141
|
+
if (entries.has(file.logicalPath)) throw new Error(`Legacy materialization manifest is ambiguous for logical path: ${file.logicalPath}`);
|
|
142
|
+
entries.set(file.logicalPath, file);
|
|
143
|
+
}
|
|
144
|
+
return entries;
|
|
145
|
+
}
|
|
146
|
+
async function removePathIfExists(path) {
|
|
147
|
+
try {
|
|
148
|
+
await rm(path);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (!(error.code === "ENOENT")) throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function shouldFormatArtifact(logicalPath) {
|
|
154
|
+
return /\.(?:[cm]?tsx?|jsx?)$/.test(logicalPath);
|
|
155
|
+
}
|
|
156
|
+
async function prepareArtifactContent(artifact) {
|
|
157
|
+
if (!shouldFormatArtifact(artifact.logicalPath)) return artifact.content;
|
|
158
|
+
let currentContent = artifact.content;
|
|
159
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
160
|
+
const result = await (await loadOxfmt()).format(artifact.logicalPath, currentContent, { singleQuote: true });
|
|
161
|
+
if (result.errors.length > 0) throw new Error(`Failed to format generated artifact ${artifact.logicalPath}: ${result.errors.join(", ")}`);
|
|
162
|
+
if (result.code === currentContent) return result.code;
|
|
163
|
+
currentContent = result.code;
|
|
164
|
+
}
|
|
165
|
+
return currentContent;
|
|
166
|
+
}
|
|
167
|
+
async function prepareArtifactsForMaterialization(artifacts) {
|
|
168
|
+
return await Promise.all(artifacts.map(async (artifact) => ({
|
|
169
|
+
artifact,
|
|
170
|
+
content: await prepareArtifactContent(artifact)
|
|
171
|
+
})));
|
|
172
|
+
}
|
|
173
|
+
async function materializeOwnedArtifacts(targetRoot, artifacts, generatorVersion, options = {}) {
|
|
174
|
+
const preparedArtifacts = await prepareArtifactsForMaterialization(artifacts);
|
|
175
|
+
const manifest = await buildMaterializationManifest(targetRoot, preparedArtifacts, generatorVersion);
|
|
176
|
+
const previousManifest = await readPersistedMaterializationManifest(targetRoot);
|
|
177
|
+
const previousOwnedEntries = previousManifest.kind === "missing" ? [] : previousManifest.kind === "legacy-v1" ? (() => {
|
|
178
|
+
indexLegacyManifestEntriesByLogicalPath(previousManifest.manifest);
|
|
179
|
+
return previousManifest.upgradedManifest.files;
|
|
180
|
+
})() : previousManifest.manifest.files;
|
|
181
|
+
const activeOwnershipScopes = new Set([...manifest.files, ...options.reconcileScopes ?? []].map((file) => ownershipScopeKey(file)));
|
|
182
|
+
const currentManifestEntriesBySourceArtifactId = new Map(manifest.files.map((file) => [file.sourceArtifactId, file]));
|
|
183
|
+
const currentManifestEntriesByScopedLogicalPath = new Set(manifest.files.map((file) => scopedLogicalPathKey(file)));
|
|
184
|
+
const plannedWrites = [];
|
|
185
|
+
const plannedDeletes = [];
|
|
186
|
+
for (const preparedArtifact of preparedArtifacts) {
|
|
187
|
+
const { artifact, content } = preparedArtifact;
|
|
188
|
+
const destinationPath = join(targetRoot, artifact.logicalPath);
|
|
189
|
+
const destinationSha256 = await readDestinationSha256(destinationPath);
|
|
190
|
+
const currentManifestEntry = currentManifestEntriesBySourceArtifactId.get(artifact.dataName);
|
|
191
|
+
if (currentManifestEntry === void 0) throw new Error(`Materialization manifest missing current entry for artifact: ${artifact.logicalPath}`);
|
|
192
|
+
const previousManifestEntry = findScopedManifestEntry(previousOwnedEntries, currentManifestEntry);
|
|
193
|
+
const action = determineMaterializationAction(destinationSha256 !== void 0, previousManifestEntry?.contentSha256, destinationSha256);
|
|
194
|
+
if (action === "conflict") throw new Error(`Materialization conflict for generator-owned file: ${artifact.logicalPath}`);
|
|
195
|
+
plannedWrites.push({
|
|
196
|
+
artifact: {
|
|
197
|
+
...artifact,
|
|
198
|
+
content
|
|
199
|
+
},
|
|
200
|
+
destinationPath,
|
|
201
|
+
action
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
for (const previousManifestEntry of previousOwnedEntries) {
|
|
205
|
+
if (!activeOwnershipScopes.has(ownershipScopeKey(previousManifestEntry))) continue;
|
|
206
|
+
if (currentManifestEntriesByScopedLogicalPath.has(scopedLogicalPathKey(previousManifestEntry))) continue;
|
|
207
|
+
const destinationPath = join(targetRoot, previousManifestEntry.logicalPath);
|
|
208
|
+
const destinationSha256 = await readDestinationSha256(destinationPath);
|
|
209
|
+
if (destinationSha256 !== void 0 && destinationSha256 !== previousManifestEntry.contentSha256) throw new Error(`Materialization conflict for stale generator-owned file: ${previousManifestEntry.logicalPath}`);
|
|
210
|
+
plannedDeletes.push({
|
|
211
|
+
logicalPath: previousManifestEntry.logicalPath,
|
|
212
|
+
destinationPath
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
for (const staleFile of plannedDeletes) await removePathIfExists(staleFile.destinationPath);
|
|
216
|
+
for (const write of plannedWrites) {
|
|
217
|
+
await mkdir(dirname(write.destinationPath), { recursive: true });
|
|
218
|
+
await writeFile(write.destinationPath, write.artifact.content, "utf-8");
|
|
219
|
+
}
|
|
220
|
+
const persistedManifestPath = getPersistedMaterializationManifestPath(targetRoot);
|
|
221
|
+
const persistedManifest = {
|
|
222
|
+
targetRoot,
|
|
223
|
+
files: [...previousOwnedEntries.filter((file) => !activeOwnershipScopes.has(ownershipScopeKey(file))), ...manifest.files]
|
|
224
|
+
};
|
|
225
|
+
await mkdir(dirname(persistedManifestPath), { recursive: true });
|
|
226
|
+
await writeFile(persistedManifestPath, `${JSON.stringify(persistedManifest, null, 2)}\n`, "utf-8");
|
|
227
|
+
return {
|
|
228
|
+
targetRoot,
|
|
229
|
+
files: [...plannedDeletes.map((staleFile) => ({
|
|
230
|
+
logicalPath: staleFile.logicalPath,
|
|
231
|
+
action: "delete"
|
|
232
|
+
})), ...plannedWrites.map((write) => ({
|
|
233
|
+
logicalPath: write.artifact.logicalPath,
|
|
234
|
+
action: write.action
|
|
235
|
+
}))]
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
//#endregion
|
|
240
|
+
export { MaterializationManifestSchema, OwnedManifestEntrySchema, buildMaterializationManifest, determineMaterializationAction, getPersistedMaterializationManifestPath, materializeOwnedArtifacts, sha256Hex, upgradeLegacyMaterializationManifest };
|