@pygmalionjs/pygmalion 0.2.25 → 0.2.26
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/node/dev-mirror.mjs +102 -3
- package/package.json +1 -1
package/node/dev-mirror.mjs
CHANGED
|
@@ -47,6 +47,74 @@ export function devMirrorRefSlug(ref) {
|
|
|
47
47
|
return createHash('sha256').update(raw).digest('hex').slice(0, 12);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Identity of the project a mirror checkout belongs to.
|
|
52
|
+
*
|
|
53
|
+
* Two worktrees of one repository configure the same mirror path, and each
|
|
54
|
+
* generates its own inventory into it. Sharing one checkout made each server see
|
|
55
|
+
* the other's generated files as unexpected changes and refuse to sync, so a
|
|
56
|
+
* checkout is claimed by the project that syncs it.
|
|
57
|
+
*/
|
|
58
|
+
export function devMirrorOwnerToken(repoRoot, appDirectory = '') {
|
|
59
|
+
const identity = `${path.resolve(repoRoot)}\u0000${appDirectory}`;
|
|
60
|
+
return createHash('sha256').update(identity).digest('hex').slice(0, 10);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The claim lives beside the repository's other Pygmalion state, not inside the
|
|
64
|
+
// checkout: a file in the worktree would itself be an unexpected change.
|
|
65
|
+
async function mirrorClaimFile(repoRoot, mirrorRoot) {
|
|
66
|
+
const directory = path.join(await pygmalionStateDirectory(repoRoot), 'mirror-claims');
|
|
67
|
+
const key = createHash('sha256').update(path.resolve(mirrorRoot)).digest('hex').slice(0, 16);
|
|
68
|
+
return path.join(directory, `${key}.json`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPlainMirrorStamp(value) {
|
|
72
|
+
return (
|
|
73
|
+
value != null &&
|
|
74
|
+
typeof value === 'object' &&
|
|
75
|
+
!Array.isArray(value) &&
|
|
76
|
+
typeof value.owner === 'string' &&
|
|
77
|
+
value.owner.length > 0
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether this project may sync the checkout at `mirrorRoot`.
|
|
83
|
+
*
|
|
84
|
+
* An unclaimed or self-claimed checkout is ours. One claimed by another project is
|
|
85
|
+
* not, and the caller moves to its own path rather than fighting over this one —
|
|
86
|
+
* which is what two worktrees of one repository were doing every time they
|
|
87
|
+
* regenerated their own inventory into the same directory.
|
|
88
|
+
*/
|
|
89
|
+
export async function devMirrorClaim(repoRoot, mirrorRoot, owner) {
|
|
90
|
+
let stamp = null;
|
|
91
|
+
try {
|
|
92
|
+
const raw = await fsp.readFile(await mirrorClaimFile(repoRoot, mirrorRoot), 'utf8');
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
stamp = isPlainMirrorStamp(parsed) ? parsed : null;
|
|
95
|
+
} catch {
|
|
96
|
+
stamp = null;
|
|
97
|
+
}
|
|
98
|
+
if (!stamp) return { owned: true, previousOwner: null };
|
|
99
|
+
return { owned: stamp.owner === owner, previousOwner: stamp.owner };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function claimDevMirror(repoRoot, mirrorRoot, owner) {
|
|
103
|
+
const file = await mirrorClaimFile(repoRoot, mirrorRoot);
|
|
104
|
+
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
105
|
+
await fsp
|
|
106
|
+
.writeFile(
|
|
107
|
+
file,
|
|
108
|
+
`${JSON.stringify({
|
|
109
|
+
owner,
|
|
110
|
+
repoRoot: path.resolve(repoRoot),
|
|
111
|
+
mirrorRoot: path.resolve(mirrorRoot),
|
|
112
|
+
claimedAt: new Date().toISOString(),
|
|
113
|
+
})}\n`,
|
|
114
|
+
)
|
|
115
|
+
.catch(() => undefined);
|
|
116
|
+
}
|
|
117
|
+
|
|
50
118
|
/**
|
|
51
119
|
* One checkout per ref. Two editor servers pointed at different refs used to
|
|
52
120
|
* share a single mirror directory and take turns running `git switch` in it, so
|
|
@@ -59,16 +127,17 @@ export function resolveDevMirrorWorktreePaths({
|
|
|
59
127
|
mirrorAppRoot,
|
|
60
128
|
appDirectory = '',
|
|
61
129
|
ref = null,
|
|
130
|
+
owner = null,
|
|
62
131
|
}) {
|
|
63
132
|
const base = path.resolve(mirrorRoot);
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
133
|
+
const parts = [devMirrorRefSlug(ref), owner ? String(owner) : ''].filter(Boolean);
|
|
134
|
+
if (!parts.length) {
|
|
66
135
|
return {
|
|
67
136
|
mirrorRoot: base,
|
|
68
137
|
mirrorAppRoot: path.resolve(mirrorAppRoot ?? path.join(base, appDirectory)),
|
|
69
138
|
};
|
|
70
139
|
}
|
|
71
|
-
const scoped = `${base}-${
|
|
140
|
+
const scoped = `${base}-${parts.join('-')}`;
|
|
72
141
|
return {
|
|
73
142
|
mirrorRoot: scoped,
|
|
74
143
|
mirrorAppRoot: path.resolve(path.join(scoped, appDirectory)),
|
|
@@ -582,6 +651,9 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
582
651
|
);
|
|
583
652
|
// The ref is repointable at runtime and each ref owns its own checkout, so the
|
|
584
653
|
// active paths are recomputed per sync instead of frozen at plugin creation.
|
|
654
|
+
const ownerToken = devMirrorOwnerToken(repoRoot, appDirectory);
|
|
655
|
+
// Set once the base checkout turns out to belong to another project.
|
|
656
|
+
let mirrorOwnerScope = null;
|
|
585
657
|
let mirrorRoot = mirrorBaseRoot;
|
|
586
658
|
let mirrorAppRoot = mirrorAppOverride ?? path.join(mirrorBaseRoot, appDirectory);
|
|
587
659
|
let viteConfig = viteConfigOverride ?? path.join(mirrorAppRoot, 'vite.config.ts');
|
|
@@ -598,6 +670,7 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
598
670
|
mirrorAppRoot: mirrorAppOverride,
|
|
599
671
|
appDirectory,
|
|
600
672
|
ref: activeRef,
|
|
673
|
+
owner: mirrorOwnerScope,
|
|
601
674
|
});
|
|
602
675
|
mirrorRoot = resolved.mirrorRoot;
|
|
603
676
|
mirrorAppRoot = resolved.mirrorAppRoot;
|
|
@@ -729,6 +802,24 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
729
802
|
const lockHash = await fileHash(lockFile);
|
|
730
803
|
const stamp = await fsp.readFile(stampFile, 'utf8').catch(() => '');
|
|
731
804
|
if ((await exists(nodeModules)) && stamp.trim() === lockHash) return false;
|
|
805
|
+
// An owner-scoped checkout is a second copy of the same source. Borrowing the
|
|
806
|
+
// base checkout's modules when the lock file agrees keeps that isolation from
|
|
807
|
+
// costing a full install per project.
|
|
808
|
+
if (!(await exists(nodeModules)) && mirrorOwnerScope) {
|
|
809
|
+
const baseModules = path.join(
|
|
810
|
+
mirrorAppOverride ?? path.join(mirrorBaseRoot, appDirectory),
|
|
811
|
+
dependencies.modulesDirectory ?? 'node_modules',
|
|
812
|
+
);
|
|
813
|
+
const baseLock = path.join(
|
|
814
|
+
mirrorAppOverride ?? path.join(mirrorBaseRoot, appDirectory),
|
|
815
|
+
dependencies.lockFile ?? 'package-lock.json',
|
|
816
|
+
);
|
|
817
|
+
const baseLockHash = await fileHash(baseLock).catch(() => null);
|
|
818
|
+
if ((await exists(baseModules)) && baseLockHash === lockHash) {
|
|
819
|
+
await fsp.symlink(baseModules, nodeModules, 'dir').catch(() => undefined);
|
|
820
|
+
if (await exists(nodeModules)) return false;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
732
823
|
await run(
|
|
733
824
|
dependencies.installCommand ?? 'npm',
|
|
734
825
|
dependencies.installArgs ?? ['install', '--no-audit', '--no-fund'],
|
|
@@ -840,6 +931,13 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
840
931
|
if (syncPromise) return syncPromise;
|
|
841
932
|
applyMirrorPathsForRef(ref);
|
|
842
933
|
syncPromise = (async () => {
|
|
934
|
+
// A checkout another project claimed is not ours to regenerate into. Moving
|
|
935
|
+
// to our own path is what ends the tug of war between two worktrees.
|
|
936
|
+
const claim = await devMirrorClaim(repoRoot, mirrorRoot, ownerToken);
|
|
937
|
+
if (!claim.owned && mirrorOwnerScope !== ownerToken) {
|
|
938
|
+
mirrorOwnerScope = ownerToken;
|
|
939
|
+
applyMirrorPathsForRef(ref);
|
|
940
|
+
}
|
|
843
941
|
status = { ...status, state: 'syncing', error: null, warning: null };
|
|
844
942
|
let warning = null;
|
|
845
943
|
try {
|
|
@@ -859,6 +957,7 @@ export function pygmalionDevMirrorPlugin(options) {
|
|
|
859
957
|
});
|
|
860
958
|
warning = mirrorState.warning;
|
|
861
959
|
const { previous, commit, shortCommit } = mirrorState;
|
|
960
|
+
await claimDevMirror(repoRoot, mirrorRoot, ownerToken);
|
|
862
961
|
const dependenciesChanged = await syncDependencies();
|
|
863
962
|
await generateInventory(commit);
|
|
864
963
|
await startPreview(dependenciesChanged);
|