@danypops/pi-packed 0.25.1 → 0.25.2
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/package.json
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from "@danypops/vehicle-server/service";
|
|
39
39
|
import { npmPackageName, readInstalledPackagesAcrossScopes, readPackageDeclarations } from "../packages/installed.ts";
|
|
40
40
|
import type { InstalledPkg } from "../packages/package.ts";
|
|
41
|
+
import { versionAtLeast } from "../publish/publish.ts";
|
|
41
42
|
|
|
42
43
|
export interface DaemonServiceManifest {
|
|
43
44
|
/** Relative to the installed package's own root directory. */
|
|
@@ -125,6 +126,47 @@ function dependsOnVehicle(pkg: InstalledPackageJson): boolean {
|
|
|
125
126
|
* web-spider, packed, enigma all expose a `serve` subcommand on the same
|
|
126
127
|
* bin their `service install` command already points at).
|
|
127
128
|
*/
|
|
129
|
+
/**
|
|
130
|
+
* Resolves ONE candidate directory to a daemon entrypoint, using the same
|
|
131
|
+
* explicit-manifest-first-then-convention rule detectVehicleDaemonService's
|
|
132
|
+
* own doc comment describes -- factored out so the nested and hoisted
|
|
133
|
+
* candidates for the same dependency name can each be resolved
|
|
134
|
+
* independently and then compared, instead of the first one found
|
|
135
|
+
* unconditionally winning (see detectVehicleDaemonService's own comment on
|
|
136
|
+
* why iteration order alone is never a safe tie-break).
|
|
137
|
+
*/
|
|
138
|
+
function resolveDependencyCandidate(depDir: string, depName: string): ResolvedDaemonEntrypoint | undefined {
|
|
139
|
+
const dep = readPackageJson(depDir);
|
|
140
|
+
if (!dep) return undefined;
|
|
141
|
+
// An explicit manifest on the dependency itself wins over convention detection here too --
|
|
142
|
+
// mirrors resolveDaemonServiceSpec's own explicit-manifest-first behavior for a directly
|
|
143
|
+
// installed package. Without this, a dependency's own correct, explicit
|
|
144
|
+
// handleFilename/name/etc. was silently discarded in favor of a guess whenever the daemon
|
|
145
|
+
// was discovered one level down instead of installed directly by name -- exactly the
|
|
146
|
+
// common case (a Pi extension like pi-papyrus has no daemon of its own). Confirmed live:
|
|
147
|
+
// papyrus's real handle file is "vehicle-handle.json", not the "daemon.json" convention
|
|
148
|
+
// guess, and papyrus is only ever discovered this way, never installed directly by name.
|
|
149
|
+
const depManifest = dep.packed?.daemonService;
|
|
150
|
+
if (depManifest && typeof depManifest.binPath === "string" && depManifest.binPath.length > 0 && dep.version) {
|
|
151
|
+
return {
|
|
152
|
+
binPath: join(depDir, depManifest.binPath),
|
|
153
|
+
args: depManifest.args,
|
|
154
|
+
name: depManifest.name ?? unscopedName(dep.name ?? depName),
|
|
155
|
+
displayName: depManifest.displayName,
|
|
156
|
+
handleFilename: depManifest.handleFilename,
|
|
157
|
+
workingDirectory: depManifest.workingDirectory,
|
|
158
|
+
restartOnFailure: depManifest.restartOnFailure,
|
|
159
|
+
restartSec: depManifest.restartSec,
|
|
160
|
+
version: dep.version,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const depBin = firstBinPath(dep);
|
|
164
|
+
if (depBin && dependsOnVehicle(dep) && dep.version) {
|
|
165
|
+
return { binPath: join(depDir, depBin), args: ["serve"], name: unscopedName(dep.name ?? depName), version: dep.version };
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
128
170
|
export function detectVehicleDaemonService(
|
|
129
171
|
packageDir: string,
|
|
130
172
|
fallbackName: string,
|
|
@@ -143,36 +185,24 @@ export function detectVehicleDaemonService(
|
|
|
143
185
|
join(packageDir, "node_modules", depName),
|
|
144
186
|
...(hoistedNodeModulesDir ? [join(hoistedNodeModulesDir, depName)] : []),
|
|
145
187
|
];
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
name: depManifest.name ?? unscopedName(dep.name ?? depName),
|
|
163
|
-
displayName: depManifest.displayName,
|
|
164
|
-
handleFilename: depManifest.handleFilename,
|
|
165
|
-
workingDirectory: depManifest.workingDirectory,
|
|
166
|
-
restartOnFailure: depManifest.restartOnFailure,
|
|
167
|
-
restartSec: depManifest.restartSec,
|
|
168
|
-
version: dep.version,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
const depBin = firstBinPath(dep);
|
|
172
|
-
if (depBin && dependsOnVehicle(dep) && dep.version) {
|
|
173
|
-
return { binPath: join(depDir, depBin), args: ["serve"], name: unscopedName(dep.name ?? depName), version: dep.version };
|
|
174
|
-
}
|
|
188
|
+
const resolved = candidateDirs
|
|
189
|
+
.map((depDir) => resolveDependencyCandidate(depDir, depName))
|
|
190
|
+
.filter((entry): entry is ResolvedDaemonEntrypoint => entry !== undefined);
|
|
191
|
+
if (resolved.length === 0) continue;
|
|
192
|
+
if (resolved.length === 1) return resolved[0];
|
|
193
|
+
// Both a nested copy (checked first, above) and a hoisted copy resolved for the exact same
|
|
194
|
+
// dependency name -- e.g. pi-papyrus's own nested @danypops/jittor@0.14.0 alongside the
|
|
195
|
+
// correctly-versioned, tree-wide hoisted @danypops/jittor@0.18.1. The real jittor incident:
|
|
196
|
+
// the nested copy always won here, unconditionally, purely because candidateDirs checks it
|
|
197
|
+
// first -- regardless of which was actually newer. Prefer the real higher semver version
|
|
198
|
+
// instead; a genuine tie keeps the first-found (nested) result, preserving papyrus's own
|
|
199
|
+
// intentional-nesting case (confirmed live, see resolveDependencyCandidate's own comment)
|
|
200
|
+
// exactly as before whenever there's nothing to disambiguate.
|
|
201
|
+
let best = resolved[0]!;
|
|
202
|
+
for (const candidate of resolved.slice(1)) {
|
|
203
|
+
if (versionAtLeast(candidate.version, best.version) && candidate.version !== best.version) best = candidate;
|
|
175
204
|
}
|
|
205
|
+
return best;
|
|
176
206
|
}
|
|
177
207
|
return undefined;
|
|
178
208
|
}
|
|
@@ -208,6 +208,60 @@ describe("resolveDaemonServiceSpec", () => {
|
|
|
208
208
|
expect(result.spec.restartSec).toBe(2);
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
it("prefers the higher-semver candidate when BOTH a nested and hoisted copy of the same dependency resolve via bin+dependency convention -- the real jittor incident: a stale nested copy must never win over a correctly-versioned hoisted one just because it's checked first", () => {
|
|
212
|
+
const piHome = fakePiHome();
|
|
213
|
+
const extDir = join(piHome, "npm", "node_modules", "@danypops/pi-papyrus");
|
|
214
|
+
writeRawPackage(extDir, { name: "@danypops/pi-papyrus", version: "1.0.0", dependencies: { "@danypops/jittor": "^0.14.0" } });
|
|
215
|
+
// A stale nested copy pi-papyrus's own outdated floor forced npm to keep private, instead of
|
|
216
|
+
// deduping to the shared, correctly-versioned, hoisted copy every other consumer already uses.
|
|
217
|
+
const nestedDir = join(extDir, "node_modules", "@danypops/jittor");
|
|
218
|
+
writeRawPackage(nestedDir, {
|
|
219
|
+
name: "@danypops/jittor",
|
|
220
|
+
version: "0.14.0",
|
|
221
|
+
bin: { jittor: "src/cli.ts" },
|
|
222
|
+
dependencies: { "@danypops/vehicle-server": "^0.3.1" },
|
|
223
|
+
});
|
|
224
|
+
const hoistedDir = join(piHome, "npm", "node_modules", "@danypops/jittor");
|
|
225
|
+
writeRawPackage(hoistedDir, {
|
|
226
|
+
name: "@danypops/jittor",
|
|
227
|
+
version: "0.18.1",
|
|
228
|
+
bin: { jittor: "src/cli.ts" },
|
|
229
|
+
dependencies: { "@danypops/vehicle-server": "^0.17.0" },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = resolveDaemonServiceSpec(piHome, "npm:@danypops/pi-papyrus");
|
|
233
|
+
expect(result.ok).toBe(true);
|
|
234
|
+
if (!result.ok) throw new Error("expected ok");
|
|
235
|
+
expect(result.spec.binPath).toBe(join(hoistedDir, "src/cli.ts"));
|
|
236
|
+
expect(result.spec.version).toBe("0.18.1");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("keeps the nested copy when it is genuinely the newer (or equal) one -- the tie-break only ever prefers a candidate that's actually newer, never blindly flips to hoisted", () => {
|
|
240
|
+
const piHome = fakePiHome();
|
|
241
|
+
const extDir = join(piHome, "npm", "node_modules", "@danypops/pi-papyrus");
|
|
242
|
+
writeRawPackage(extDir, { name: "@danypops/pi-papyrus", version: "1.0.0", dependencies: { "@danypops/jittor": "^0.18.0" } });
|
|
243
|
+
const nestedDir = join(extDir, "node_modules", "@danypops/jittor");
|
|
244
|
+
writeRawPackage(nestedDir, {
|
|
245
|
+
name: "@danypops/jittor",
|
|
246
|
+
version: "0.18.1",
|
|
247
|
+
bin: { jittor: "src/cli.ts" },
|
|
248
|
+
dependencies: { "@danypops/vehicle-server": "^0.17.0" },
|
|
249
|
+
});
|
|
250
|
+
const hoistedDir = join(piHome, "npm", "node_modules", "@danypops/jittor");
|
|
251
|
+
writeRawPackage(hoistedDir, {
|
|
252
|
+
name: "@danypops/jittor",
|
|
253
|
+
version: "0.14.0",
|
|
254
|
+
bin: { jittor: "src/cli.ts" },
|
|
255
|
+
dependencies: { "@danypops/vehicle-server": "^0.3.1" },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const result = resolveDaemonServiceSpec(piHome, "npm:@danypops/pi-papyrus");
|
|
259
|
+
expect(result.ok).toBe(true);
|
|
260
|
+
if (!result.ok) throw new Error("expected ok");
|
|
261
|
+
expect(result.spec.binPath).toBe(join(nestedDir, "src/cli.ts"));
|
|
262
|
+
expect(result.spec.version).toBe("0.18.1");
|
|
263
|
+
});
|
|
264
|
+
|
|
211
265
|
it("also detects the legacy @danypops/daemon-kit dependency name, not just vehicle-server", () => {
|
|
212
266
|
const piHome = fakePiHome();
|
|
213
267
|
const dir = join(piHome, "npm", "node_modules", "@danypops/web-spider-daemon");
|