Plugin entry resolution now avoids stale gitignored dist output in dev worktrees. - Prefer bundled plugin entries when present, preserving published tarball behavior. - Choose src/index.ts over dist/index.js when source files are newer than dist output. - Cover CLI and core plugin loaders with freshness and sync tests. - Add a patch changeset for the published CLI package. Files changed: .changeset/fn-6615-prefer-fresh-src.md | 5 ++ .../__tests__/bundled-plugin-install.test.ts | 46 ++++++++--- .../resolve-plugin-entry-path-sync.test.ts | 50 ++++++++++-- packages/cli/src/plugins/bundled-plugin-install.ts | 93 ++++++++++++++++++---- packages/core/src/__tests__/plugin-loader.test.ts | 71 ++++++++++++++++- packages/core/src/plugin-loader.ts | 93 ++++++++++++++++++---- 6 files changed, 313 insertions(+), 45 deletions(-) Fusion-Task-Id: FN-6615 Fusion-Task-Lineage: 3d9abfa8-a405-47fa-8330-451c26a0ee75
98 lines
3.8 KiB
TypeScript
98 lines
3.8 KiB
TypeScript
/**
|
|
* Drift guard for the intentionally duplicated resolvePluginEntryPath.
|
|
*
|
|
* The CLI keeps a local copy in bundled-plugin-install.ts (so its fs mocks
|
|
* work in tests) while @fusion/core owns the copy used by the dashboard
|
|
* install/enable routes. This test runs both against real on-disk layouts and
|
|
* asserts identical results, so a candidate-list change applied to one copy
|
|
* but not the other fails CI instead of silently diverging.
|
|
*
|
|
* No fs mocks here on purpose — vitest module mocks don't reach the
|
|
* externalized @fusion/core import, so real temp directories are the only
|
|
* seam that exercises both implementations equally.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, utimesSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { resolvePluginEntryPath as cliResolve } from "../bundled-plugin-install.js";
|
|
import { resolvePluginEntryPath as coreResolve } from "@fusion/core";
|
|
|
|
describe("resolvePluginEntryPath: CLI copy stays in sync with @fusion/core", () => {
|
|
let dir: string;
|
|
|
|
beforeEach(() => {
|
|
dir = mkdtempSync(join(tmpdir(), "entry-path-sync-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
function touch(relative: string) {
|
|
const full = join(dir, relative);
|
|
mkdirSync(dirname(full), { recursive: true });
|
|
writeFileSync(full, "// entry\n");
|
|
}
|
|
|
|
const older = new Date("2026-01-01T00:00:00.000Z");
|
|
const newer = new Date("2026-01-01T00:01:00.000Z");
|
|
|
|
const layouts: Array<{
|
|
name: string;
|
|
files: string[];
|
|
expected: string | null;
|
|
mtimes?: Record<string, Date>;
|
|
}> = [
|
|
{ name: "bundled.js only", files: ["bundled.js"], expected: "bundled.js" },
|
|
{ name: "dist/index.js only", files: ["dist/index.js"], expected: "dist/index.js" },
|
|
{ name: "src/index.ts only", files: ["src/index.ts"], expected: "src/index.ts" },
|
|
{ name: "bundled.js preferred over src", files: ["bundled.js", "src/index.ts"], expected: "bundled.js" },
|
|
{
|
|
name: "dist + src, src newer → src/index.ts",
|
|
files: ["dist/index.js", "src/index.ts"],
|
|
expected: "src/index.ts",
|
|
mtimes: { "dist/index.js": older, "src/index.ts": newer },
|
|
},
|
|
{
|
|
name: "dist + src, dist newer → dist/index.js",
|
|
files: ["dist/index.js", "src/index.ts"],
|
|
expected: "dist/index.js",
|
|
mtimes: { "dist/index.js": newer, "src/index.ts": older },
|
|
},
|
|
{
|
|
name: "dist + src, equal mtimes → dist/index.js",
|
|
files: ["dist/index.js", "src/index.ts"],
|
|
expected: "dist/index.js",
|
|
mtimes: { "dist/index.js": older, "src/index.ts": older },
|
|
},
|
|
{
|
|
name: "dist + src, non-index src file newer → src/index.ts",
|
|
files: ["dist/index.js", "src/index.ts", "src/settings.ts"],
|
|
expected: "src/index.ts",
|
|
mtimes: { "dist/index.js": older, "src/index.ts": older, "src/settings.ts": newer },
|
|
},
|
|
{
|
|
name: "bundled.js + dist + src, src newer → bundled.js",
|
|
files: ["bundled.js", "dist/index.js", "src/index.ts"],
|
|
expected: "bundled.js",
|
|
mtimes: { "dist/index.js": older, "src/index.ts": newer },
|
|
},
|
|
{ name: "all three → bundled.js", files: ["bundled.js", "dist/index.js", "src/index.ts"], expected: "bundled.js" },
|
|
{ name: "no entry files", files: ["README.md"], expected: null },
|
|
];
|
|
|
|
for (const layout of layouts) {
|
|
it(`resolves identically for: ${layout.name}`, () => {
|
|
for (const f of layout.files) touch(f);
|
|
for (const [file, mtime] of Object.entries(layout.mtimes ?? {})) {
|
|
utimesSync(join(dir, file), mtime, mtime);
|
|
}
|
|
const expected = layout.expected === null ? null : join(dir, layout.expected);
|
|
|
|
expect(cliResolve(dir)).toBe(expected);
|
|
expect(coreResolve(dir)).toBe(expected);
|
|
});
|
|
}
|
|
});
|