



















@@ -149,6 +149,11 @@ async function createExistingInstallFixture(fixtureRoot: string) {
149149return { installBaseDir, sourceDir, targetDir };
150150}
151151152+async function addHardlinkedFile(filePath: string, linkPath: string): Promise<void> {
153+await fs.mkdir(path.dirname(linkPath), { recursive: true });
154+await fs.link(filePath, linkPath);
155+}
156+152157async function createReboundInstallFixture(params: {
153158fixtureRoot: string;
154159withExistingInstall?: boolean;
@@ -296,6 +301,156 @@ describe("installPackageDir", () => {
296301).resolves.toHaveLength(0);
297302});
298303304+it.runIf(process.platform !== "win32")(
305+"updates package-manager installs that contain hardlinked package files",
306+async () => {
307+await fixtureRootTracker.setup();
308+const fixtureRoot = await fixtureRootTracker.make("case");
309+const { sourceDir, targetDir } = await createExistingInstallFixture(fixtureRoot);
310+await addHardlinkedFile(
311+path.join(targetDir, "marker.txt"),
312+path.join(fixtureRoot, "cache", "existing-marker.txt"),
313+);
314+315+vi.mocked(runCommandWithTimeout).mockImplementation(async (_argv, optionsOrTimeout) => {
316+const cwd = typeof optionsOrTimeout === "number" ? undefined : optionsOrTimeout.cwd;
317+if (cwd === undefined) {
318+throw new Error("expected staged package install cwd");
319+}
320+const depFile = path.join(cwd, "node_modules", "demo-dep", "index.js");
321+await fs.mkdir(path.dirname(depFile), { recursive: true });
322+await fs.writeFile(depFile, "module.exports = 1;\n", "utf8");
323+await addHardlinkedFile(depFile, path.join(fixtureRoot, "cache", "staged-dep.js"));
324+return {
325+stdout: "",
326+stderr: "",
327+code: 0,
328+signal: null,
329+killed: false,
330+termination: "exit",
331+};
332+});
333+334+const result = await installPackageDir({
335+ sourceDir,
336+ targetDir,
337+mode: "update",
338+timeoutMs: 1_000,
339+copyErrorPrefix: "failed to copy plugin",
340+hasDeps: true,
341+sourceHardlinks: "package-manager",
342+depsLogMessage: "Installing deps…",
343+});
344+345+expect(result).toEqual({ ok: true });
346+await expect(fs.readFile(path.join(targetDir, "marker.txt"), "utf8")).resolves.toBe("new");
347+await expect(
348+fs.lstat(path.join(targetDir, "node_modules", "demo-dep", "index.js")),
349+).resolves.toMatchObject({ nlink: 2 });
350+},
351+);
352+353+it.runIf(process.platform !== "win32")(
354+"requires explicit package-manager hardlink allowance for dependency installs",
355+async () => {
356+await fixtureRootTracker.setup();
357+const fixtureRoot = await fixtureRootTracker.make("case");
358+const { sourceDir, targetDir } = await createExistingInstallFixture(fixtureRoot);
359+await addHardlinkedFile(
360+path.join(targetDir, "marker.txt"),
361+path.join(fixtureRoot, "cache", "existing-marker.txt"),
362+);
363+364+vi.mocked(runCommandWithTimeout).mockResolvedValue({
365+stdout: "",
366+stderr: "",
367+code: 0,
368+signal: null,
369+killed: false,
370+termination: "exit",
371+});
372+373+const result = await installPackageDir({
374+ sourceDir,
375+ targetDir,
376+mode: "update",
377+timeoutMs: 1_000,
378+copyErrorPrefix: "failed to copy plugin",
379+hasDeps: true,
380+depsLogMessage: "Installing deps…",
381+});
382+383+expect(result.ok).toBe(false);
384+if (!result.ok) {
385+expect(result.error).toContain("Hardlinked source file is not allowed");
386+}
387+await expect(fs.readFile(path.join(targetDir, "marker.txt"), "utf8")).resolves.toBe("old");
388+},
389+);
390+391+it.runIf(process.platform !== "win32")(
392+"keeps hardlinked existing installs rejected for dependency-free updates",
393+async () => {
394+await fixtureRootTracker.setup();
395+const fixtureRoot = await fixtureRootTracker.make("case");
396+const { sourceDir, targetDir } = await createExistingInstallFixture(fixtureRoot);
397+await addHardlinkedFile(
398+path.join(targetDir, "marker.txt"),
399+path.join(fixtureRoot, "cache", "existing-marker.txt"),
400+);
401+402+const result = await installPackageDir({
403+ sourceDir,
404+ targetDir,
405+mode: "update",
406+timeoutMs: 1_000,
407+copyErrorPrefix: "failed to copy plugin",
408+hasDeps: false,
409+depsLogMessage: "Installing deps…",
410+});
411+412+expect(result.ok).toBe(false);
413+if (!result.ok) {
414+expect(result.error).toContain("Hardlinked source file is not allowed");
415+}
416+await expect(fs.readFile(path.join(targetDir, "marker.txt"), "utf8")).resolves.toBe("old");
417+},
418+);
419+420+it.runIf(process.platform !== "win32")(
421+"keeps hardlinked staged installs rejected for dependency-free publishes",
422+async () => {
423+await fixtureRootTracker.setup();
424+const fixtureRoot = await fixtureRootTracker.make("case");
425+const sourceDir = path.join(fixtureRoot, "source");
426+const targetDir = path.join(fixtureRoot, "plugins", "demo");
427+await fs.mkdir(sourceDir, { recursive: true });
428+await fs.writeFile(path.join(sourceDir, "marker.txt"), "new");
429+430+const result = await installPackageDir({
431+ sourceDir,
432+ targetDir,
433+mode: "install",
434+timeoutMs: 1_000,
435+copyErrorPrefix: "failed to copy plugin",
436+hasDeps: false,
437+depsLogMessage: "Installing deps…",
438+afterCopy: async (installedDir) => {
439+await addHardlinkedFile(
440+path.join(installedDir, "marker.txt"),
441+path.join(fixtureRoot, "cache", "staged-marker.txt"),
442+);
443+},
444+});
445+446+expect(result.ok).toBe(false);
447+if (!result.ok) {
448+expect(result.error).toContain("Hardlinked source file is not allowed");
449+}
450+await expectMissingPath(path.join(targetDir, "marker.txt"));
451+},
452+);
453+299454it("aborts without outside writes when the install base is rebound before publish", async () => {
300455await fixtureRootTracker.setup();
301456const fixtureRoot = await fixtureRootTracker.make("case");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。