Keep P2P consumers on the current transport

This commit is contained in:
vorotamoroz
2026-07-20 15:06:05 +00:00
parent 893c08ad2a
commit 6484a43e2f
34 changed files with 731 additions and 73 deletions
+3
View File
@@ -1,6 +1,7 @@
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { NodeServiceContext } from "@/apps/cli/services/NodeServiceContext";
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
export type CLICommand =
| "daemon"
@@ -48,6 +49,8 @@ export interface CLICommandContext {
databasePath: string;
vaultPath: string;
core: LiveSyncBaseCore<NodeServiceContext, never>;
/** Current-result contract owned by the P2P service feature. */
p2pReplicator?: UseP2PReplicatorResult;
settingsPath: string;
originalSyncSettings: Pick<
ObsidianLiveSyncSettings,
+19 -4
View File
@@ -19,11 +19,12 @@ import {
} from "octagonal-wheels/common/logger";
import { runCommand } from "./commands/runCommand";
import { isCLICommand } from "./commands/types";
import type { CLICommand, CLIOptions } from "./commands/types";
import type { CLICommand, CLICommandContext, CLIOptions } from "./commands/types";
import { getPathFromUXFileInfo } from "@vrtmrz/livesync-commonlib/compat/common/typeUtils";
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
import { IgnoreRules } from "./serviceModules/IgnoreRules";
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature";
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
import { createNodeStandardIo, fsPromises as fs, path, fs as fsSync } from "@vrtmrz/livesync-commonlib/node";
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context";
import { writeStderrLine, writeStdoutLine } from "./cliOutput";
@@ -33,6 +34,9 @@ const SETTINGS_FILE = ".livesync/settings.json";
ensureGlobalNodeLocalStorage();
defaultLoggerEnv.minLogLevel = LOG_LEVEL_DEBUG;
/** Injectable command boundary used by CLI integration probes. */
export type CliCommandRunner = (options: CLIOptions, context: CLICommandContext) => Promise<boolean>;
function printHelp(standardIo: StandardIo): void {
writeStdoutLine(
standardIo,
@@ -264,7 +268,10 @@ async function createDefaultSettingsFile(options: CLIOptions, standardIo: Standa
writeStdoutLine(standardIo, `[Done] Created settings file: ${targetPath}`);
}
export async function main(standardIo: StandardIo = createNodeStandardIo()) {
export async function main(
standardIo: StandardIo = createNodeStandardIo(),
commandRunner: CliCommandRunner = runCommand
) {
const options = parseArgs(standardIo);
if (options.interval && options.command !== "daemon") {
writeStderrLine(standardIo, `Warning: --interval is only used in daemon mode, ignored for '${options.command}'`);
@@ -438,6 +445,7 @@ export async function main(standardIo: StandardIo = createNodeStandardIo()) {
);
// Create LiveSync core
let p2pReplicator: UseP2PReplicatorResult | undefined;
const core = new LiveSyncBaseCore(
serviceHubInstance,
(core: LiveSyncBaseCore<NodeServiceContext, never>, serviceHub: InjectableServiceHub<NodeServiceContext>) => {
@@ -447,7 +455,7 @@ export async function main(standardIo: StandardIo = createNodeStandardIo()) {
() => [], // No add-ons
(core) => {
// Register P2P replicator feature.
useP2PReplicatorFeature(core);
p2pReplicator = useP2PReplicatorFeature(core);
// Add target filter to prevent internal files are handled
core.services.vault.isTargetFile.addHandler(async (target) => {
const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target));
@@ -560,7 +568,14 @@ export async function main(standardIo: StandardIo = createNodeStandardIo()) {
infoLog("");
}
const result = await runCommand(options, { databasePath, vaultPath, core, settingsPath, originalSyncSettings });
const result = await commandRunner(options, {
databasePath,
vaultPath,
core,
p2pReplicator,
settingsPath,
originalSyncSettings,
});
if (!result) {
writeStderrLine(standardIo, `[Error] Command '${options.command}' failed`);
process.exitCode = 1;
@@ -0,0 +1,39 @@
#!/usr/bin/env node
import { RTCPeerConnection } from "werift";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import { createNodeStandardIo } from "@vrtmrz/livesync-commonlib/node";
import { writeStderrLine } from "@/apps/cli/cliOutput";
import { main, type CliCommandRunner } from "@/apps/cli/main";
import { parseTimeoutSeconds } from "@/apps/cli/commands/p2p";
import { runP2PReplicatorReplacementProbe } from "./p2p-replicator-replacement";
if (
typeof (compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection === "undefined" &&
typeof RTCPeerConnection === "function"
) {
(compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection = RTCPeerConnection;
}
const standardIo = createNodeStandardIo();
const runLifecycleProbe: CliCommandRunner = async (options, context) => {
if (options.command !== "p2p-sync" || options.commandArgs.length < 2) {
throw new Error("The P2P lifecycle test entry requires: p2p-sync <peer> <timeout> [note-path] [note-content]");
}
const peerToken = options.commandArgs[0].trim();
if (!peerToken) {
throw new Error("The P2P lifecycle test entry requires a non-empty peer");
}
const timeoutSec = parseTimeoutSeconds(options.commandArgs[1], "P2P lifecycle test entry");
return await runP2PReplicatorReplacementProbe(
context,
peerToken,
timeoutSec * 1000,
options.commandArgs[2],
options.commandArgs[3]
);
};
main(standardIo, runLifecycleProbe).catch((error) => {
writeStderrLine(standardIo, "[Fatal Error]", error);
process.exit(1);
});
@@ -0,0 +1,132 @@
import type { FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import type { CLICommandContext } from "@/apps/cli/commands/types";
import { openP2PHost } from "@/apps/cli/commands/p2p";
const DEFAULT_NOTE_PATH = "p2p-replicator-replacement.md";
const DEFAULT_NOTE_CONTENT = "Replicated after replacing the active P2P replicator.";
function delay(ms: number): Promise<void> {
return new Promise((resolve) => compatGlobal.setTimeout(resolve, ms));
}
function describeError(value: unknown): string {
return value instanceof Error ? (value.stack ?? value.message) : String(value);
}
async function waitForServing(replicator: LiveSyncTrysteroReplicator, timeoutMs: number): Promise<void> {
const started = Date.now();
while (Date.now() - started <= timeoutMs) {
if (replicator.server?.isServing) return;
await delay(200);
}
throw new Error("The replacement P2P replicator did not start serving within the timeout");
}
async function waitForPeer(
replicator: LiveSyncTrysteroReplicator,
targetPeer: string,
timeoutMs: number
): Promise<{ peerId: string; name: string }> {
const started = Date.now();
while (Date.now() - started <= timeoutMs) {
const peer = replicator.knownAdvertisements.find(
(candidate) => candidate.name === targetPeer || candidate.peerId === targetPeer
);
if (peer) return peer;
await delay(200);
}
const knownPeers = replicator.knownAdvertisements.map((peer) => `${peer.name} (${peer.peerId})`).join(", ");
throw new Error(
`Peer '${targetPeer}' was not discovered within the timeout. Known peers: ${knownPeers || "none"}`
);
}
function assertPullSucceeded(result: unknown): void {
if (result && typeof result === "object" && "error" in result && result.error) {
throw new Error(`P2P pull failed: ${describeError(result.error)}`);
}
}
async function communicateWithPeer(
replicator: LiveSyncTrysteroReplicator,
targetPeer: string,
timeoutMs: number
): Promise<{ peerId: string; name: string }> {
await replicator.open();
await waitForServing(replicator, timeoutMs);
const peer = await waitForPeer(replicator, targetPeer, timeoutMs);
assertPullSucceeded(await replicator.replicateFrom(peer.peerId, false));
const pushResult = await replicator.requestSynchroniseToPeer(peer.peerId);
if (!pushResult || pushResult.ok !== true) {
throw new Error(`P2P push failed: ${describeError(pushResult?.error)}`);
}
return peer;
}
/** Runs the real-transport lifecycle probe used by the Deno and Compose P2P suites. */
export async function runP2PReplicatorReplacementProbe(
context: CLICommandContext,
targetPeer: string,
timeoutMs: number,
notePath = DEFAULT_NOTE_PATH,
noteContent = DEFAULT_NOTE_CONTENT
): Promise<boolean> {
const { core, p2pReplicator } = context;
if (!p2pReplicator) {
throw new Error("The CLI did not expose its P2P service-feature result to the integration probe");
}
const firstReplicator = await openP2PHost(core);
if (p2pReplicator.replicator !== firstReplicator) {
throw new Error("The P2P service feature did not expose the newly created replicator");
}
const firstPeer = await communicateWithPeer(firstReplicator, targetPeer, timeoutMs);
const initialised = await core.services.databaseEvents.initialiseDatabase(false, true, false);
if (!initialised) {
throw new Error("Database reinitialisation failed during the P2P replacement probe");
}
const replacementReplicator = p2pReplicator.replicator;
if (core.services.replicator.getActiveReplicator() !== replacementReplicator) {
throw new Error("ReplicatorService did not activate the P2P service feature's replacement replicator");
}
if (replacementReplicator === firstReplicator) {
throw new Error("Database reinitialisation retained the previous P2P replicator instance");
}
if (firstReplicator.server !== undefined) {
throw new Error("The previous P2P replicator remained open after replacement");
}
const settings = core.services.setting.currentSettings();
settings.P2P_AutoStart = true;
await core.services.control.applySettings();
const resumedReplicator = p2pReplicator.replicator;
await waitForServing(resumedReplicator, timeoutMs);
if (firstReplicator.server !== undefined) {
throw new Error("A setting event reopened the previous P2P replicator");
}
const encoded = new TextEncoder().encode(noteContent);
const noteBody = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
const timestamp = Date.now();
await core.serviceModules.storageAccess.writeFileAuto(notePath, noteBody, {
ctime: timestamp,
mtime: timestamp,
});
await core.serviceModules.fileHandler.storeFileToDB(notePath as FilePathWithPrefix, true);
const replacementPeer = await communicateWithPeer(resumedReplicator, targetPeer, timeoutMs);
if (replacementPeer.name !== firstPeer.name) {
throw new Error(
`The replacement replicator reached '${replacementPeer.name}' instead of the original peer '${firstPeer.name}'`
);
}
core.services.context.standardIo.writeStdout(
`[Probe] P2P replicator replaced, old transport stayed closed, and ${notePath} was sent through the replacement.\n`
);
return true;
}
+3
View File
@@ -15,6 +15,9 @@
"test:p2p-host": "deno test --env-file=.test.env -A --no-check test-p2p-host.ts",
"test:p2p-peers": "deno test --env-file=.test.env -A --no-check test-p2p-peers-local-relay.ts",
"test:p2p-sync": "deno test --env-file=.test.env -A --no-check test-p2p-sync.ts",
"test:p2p-replacement": "deno test --env-file=.test.env -A --no-check test-p2p-replicator-replacement.ts",
"test:p2p-relay-disconnect": "deno test --env-file=.test.env -A --no-check test-p2p-relay-disconnect.ts",
"test:p2p:ci": "deno test --env-file=.test.env -A --no-check test-p2p-sync.ts test-p2p-replicator-replacement.ts test-p2p-relay-disconnect.ts",
"test:p2p-three-nodes": "deno test --env-file=.test.env -A --no-check test-p2p-three-nodes-conflict.ts",
"test:p2p-upload-download": "deno test --env-file=.test.env -A --no-check test-p2p-upload-download-repro.ts",
"test:benchmark-contract": "deno test --env-file=.test.env -A --no-check test-benchmark-contract.ts",
@@ -0,0 +1,82 @@
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import { RTCPeerConnection } from "werift";
import { TrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
const requireFromProbe = createRequire(import.meta.url);
const commonlibEntry = requireFromProbe.resolve("@vrtmrz/livesync-commonlib/context");
const requireFromCommonlib = createRequire(commonlibEntry);
const nostrEntry = requireFromCommonlib.resolve("@trystero-p2p/nostr");
const { getRelaySockets, joinRoom, pauseRelayReconnection } = await import(pathToFileURL(nostrEntry).href);
const relayUrl = process.env.RELAY ?? "ws://nostr-relay:7777/";
const timeoutMs = Number(process.env.RELAY_TIMEOUT_MS ?? 15_000);
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitFor(description, predicate) {
const deadline = Date.now() + timeoutMs;
while (Date.now() <= deadline) {
if (predicate()) return;
await delay(50);
}
throw new Error(`Timed out waiting for ${description}`);
}
const room = joinRoom(
{
appId: `livesync-relay-disconnect-probe-${Date.now()}`,
password: "local-test-only",
relayConfig: {
urls: [relayUrl],
manualReconnection: true,
},
rtcPolyfill: RTCPeerConnection,
},
"disconnect-probe"
);
try {
await waitFor("the relay WebSocket to open", () =>
Object.values(getRelaySockets()).some((socket) => socket.readyState === WebSocket.OPEN)
);
const originalSockets = Object.values(getRelaySockets());
const replicator = new TrysteroReplicator(
{},
{
close: async () => undefined,
dispatchConnectionStatus: async () => undefined,
}
);
replicator.disconnectFromServer();
await waitFor("all relay WebSockets to close", () =>
originalSockets.every((socket) => socket.readyState === WebSocket.CLOSED)
);
await delay(4_000);
if (!originalSockets.every((socket) => socket.readyState === WebSocket.CLOSED)) {
throw new Error("A relay WebSocket reconnected while reconnection was paused");
}
replicator.allowReconnection();
await waitFor("a replacement relay WebSocket to open", () =>
Object.values(getRelaySockets()).some(
(socket) => !originalSockets.includes(socket) && socket.readyState === WebSocket.OPEN
)
);
console.log(
JSON.stringify({
socketsClosed: originalSockets.length,
stayedDisconnectedWhilePaused: true,
reconnectedAfterResume: true,
})
);
} finally {
await room.leave();
pauseRelayReconnection();
for (const socket of Object.values(getRelaySockets())) socket.close();
}
+3 -3
View File
@@ -1,15 +1,15 @@
#!/usr/bin/env sh
set -eu
TASK="${CLI_E2E_TASK:-test:p2p-sync}"
TASK="${CLI_E2E_TASK:-test:p2p:ci}"
case "$TASK" in
test:p2p-host|test:p2p-peers|test:p2p-sync|test:p2p-three-nodes|test:p2p-upload-download)
test:p2p-host|test:p2p-peers|test:p2p-sync|test:p2p-replacement|test:p2p-relay-disconnect|test:p2p:ci|test:p2p-three-nodes|test:p2p-upload-download)
exec deno task "$TASK"
;;
*)
echo "Unknown CLI_E2E_TASK: $TASK" >&2
echo "Expected one of: test:p2p-host, test:p2p-peers, test:p2p-sync, test:p2p-three-nodes, test:p2p-upload-download" >&2
echo "Expected one of: test:p2p-host, test:p2p-peers, test:p2p-sync, test:p2p-replacement, test:p2p-relay-disconnect, test:p2p:ci, test:p2p-three-nodes, test:p2p-upload-download" >&2
exit 2
;;
esac
+3 -1
View File
@@ -1,7 +1,7 @@
const repositoryRoot = await Deno.realPath(new URL("../../../../", import.meta.url));
const composeArgs = ["compose", "-f", "test/bench-network/compose.yml"];
const p2pEnvironment = {
CLI_E2E_TASK: Deno.env.get("CLI_E2E_TASK") ?? "test:p2p-sync",
CLI_E2E_TASK: Deno.env.get("CLI_E2E_TASK") ?? "test:p2p:ci",
RELAY: Deno.env.get("RELAY") ?? "ws://nostr-relay:7777/",
PEERS_TIMEOUT: Deno.env.get("PEERS_TIMEOUT") ?? "20",
SYNC_TIMEOUT: Deno.env.get("SYNC_TIMEOUT") ?? "60",
@@ -10,6 +10,8 @@ const p2pEnvironment = {
LIVESYNC_P2P_PEERS_RETRY: Deno.env.get("LIVESYNC_P2P_PEERS_RETRY") ?? "1",
LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: Deno.env.get("LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS") ?? "60000",
BENCH_LIVESYNC_TEST_TEE: Deno.env.get("BENCH_LIVESYNC_TEST_TEE") ?? "0",
LIVESYNC_CLI_DEBUG: Deno.env.get("LIVESYNC_CLI_DEBUG") ?? "0",
LIVESYNC_CLI_VERBOSE: Deno.env.get("LIVESYNC_CLI_VERBOSE") ?? "0",
};
async function runDocker(args: string[], env?: Record<string, string>): Promise<Deno.CommandStatus> {
@@ -0,0 +1,26 @@
import { assert, assertEquals } from "@std/assert";
Deno.test("p2p lifecycle: explicit disconnect closes and pauses relay WebSockets", async () => {
const command = new Deno.Command("node", {
args: [new URL("./relay-disconnect-probe.mjs", import.meta.url).pathname],
env: {
RELAY: Deno.env.get("RELAY") ?? "ws://nostr-relay:7777/",
RELAY_TIMEOUT_MS: Deno.env.get("LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS") ?? "15000",
},
stdout: "piped",
stderr: "piped",
});
const result = await command.output();
const stdout = new TextDecoder().decode(result.stdout).trim();
const stderr = new TextDecoder().decode(result.stderr).trim();
assert(result.success, `Relay disconnect probe failed\nstdout: ${stdout}\nstderr: ${stderr}`);
const report = JSON.parse(stdout.split("\n").at(-1) ?? "{}") as {
socketsClosed?: number;
stayedDisconnectedWhilePaused?: boolean;
reconnectedAfterResume?: boolean;
};
assert((report.socketsClosed ?? 0) > 0, "The probe did not observe an open relay WebSocket");
assertEquals(report.stayedDisconnectedWhilePaused, true);
assertEquals(report.reconnectedAfterResume, true);
});
@@ -0,0 +1,117 @@
import { assert, assertEquals, assertStringIncludes } from "@std/assert";
import { join } from "@std/path";
import { TempDir } from "./helpers/temp.ts";
import { initSettingsFile, applyP2pSettings, applyP2pTestTweaks } from "./helpers/settings.ts";
import { startCliInBackground } from "./helpers/backgroundCli.ts";
import { maybeStartLocalRelay, stopLocalRelayIfStarted, maybeStartCoturn, stopCoturnIfStarted } from "./helpers/p2p.ts";
import { CLI_DIR, runCli, sanitiseCatStdout } from "./helpers/cli.ts";
const NOTE_PATH = "p2p-replicator-replacement.md";
const NOTE_CONTENT = "Replicated after replacing the active P2P replicator.";
async function runReplacementProbe(
vaultPath: string,
settingsPath: string,
targetPeer: string,
timeoutMs: number
): Promise<{ code: number; stdout: string; stderr: string }> {
const command = new Deno.Command("node", {
args: [
join(CLI_DIR, "dist", "p2p-lifecycle-test.cjs"),
vaultPath,
"--settings",
settingsPath,
"p2p-sync",
targetPeer,
String(timeoutMs / 1000),
NOTE_PATH,
NOTE_CONTENT,
],
cwd: CLI_DIR,
stdout: "piped",
stderr: "piped",
});
const result = await command.output();
return {
code: result.code,
stdout: new TextDecoder().decode(result.stdout),
stderr: new TextDecoder().decode(result.stderr),
};
}
Deno.test("p2p lifecycle: replacement keeps real CLI communication on the current replicator", async () => {
const relay = Deno.env.get("RELAY") ?? "ws://localhost:4000/";
const peersTimeout = Number(Deno.env.get("PEERS_TIMEOUT") ?? "20");
const syncTimeout = Number(Deno.env.get("SYNC_TIMEOUT") ?? "60");
const probeTimeoutMs = Math.max(peersTimeout, syncTimeout) * 1000;
const nonce = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
const roomId = Deno.env.get("ROOM_ID") ?? `replacement-room-${nonce}`;
const passphrase = Deno.env.get("PASSPHRASE") ?? `replacement-pass-${nonce}`;
const appId = "self-hosted-livesync-cli-replacement-test";
const hostPeerName = `p2p-replacement-host-${nonce}`;
const probePeerName = `p2p-replacement-probe-${nonce}`;
const verifierPeerName = `p2p-replacement-verifier-${nonce}`;
const useCoturn = Deno.env.get("LIVESYNC_USE_COTURN") !== "0";
const turnServers = Deno.env.get("TURN_SERVERS") ?? (useCoturn ? "turn:127.0.0.1:3478" : "none");
await using workDir = await TempDir.create("livesync-cli-p2p-replacement");
const hostVault = workDir.join("vault-host");
const probeVault = workDir.join("vault-probe");
const verifierVault = workDir.join("vault-verifier");
const hostSettings = workDir.join("settings-host.json");
const probeSettings = workDir.join("settings-probe.json");
const verifierSettings = workDir.join("settings-verifier.json");
await Promise.all([
Deno.mkdir(hostVault, { recursive: true }),
Deno.mkdir(probeVault, { recursive: true }),
Deno.mkdir(verifierVault, { recursive: true }),
]);
const relayStarted = await maybeStartLocalRelay(relay);
const coturnStarted = await maybeStartCoturn(turnServers);
try {
for (const settingsPath of [hostSettings, probeSettings, verifierSettings]) {
await initSettingsFile(settingsPath);
await applyP2pSettings(settingsPath, roomId, passphrase, appId, relay, "~.*", turnServers);
}
await applyP2pTestTweaks(hostSettings, hostPeerName, passphrase);
await applyP2pTestTweaks(probeSettings, probePeerName, passphrase);
await applyP2pTestTweaks(verifierSettings, verifierPeerName, passphrase);
const host = startCliInBackground(hostVault, "--settings", hostSettings, "p2p-host");
try {
await host.waitUntilContains("P2P host is running", 20000);
const probe = await runReplacementProbe(probeVault, probeSettings, hostPeerName, probeTimeoutMs);
assert(
probe.code === 0,
`P2P replacement probe failed\nstdout: ${probe.stdout}\nstderr: ${probe.stderr}`
);
assertStringIncludes(probe.stdout, "[Probe] P2P replicator replaced");
const syncResult = await runCli(
verifierVault,
"--settings",
verifierSettings,
"p2p-sync",
hostPeerName,
String(syncTimeout)
);
assert(
syncResult.code === 0,
`Verifier P2P sync failed\nstdout: ${syncResult.stdout}\nstderr: ${syncResult.stderr}`
);
const catResult = await runCli(verifierVault, "--settings", verifierSettings, "cat", NOTE_PATH);
assert(
catResult.code === 0,
`Verifier could not read ${NOTE_PATH}\nstdout: ${catResult.stdout}\nstderr: ${catResult.stderr}`
);
assertEquals(sanitiseCatStdout(catResult.stdout).trim(), NOTE_CONTENT);
} finally {
await host.stop();
}
} finally {
await stopLocalRelayIfStarted(relayStarted);
await stopCoturnIfStarted(coturnStarted);
}
});
+10 -5
View File
@@ -45,7 +45,7 @@ function injectBanner(): import("vite").Plugin {
name: "inject-banner",
generateBundle(_options, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === "chunk" && chunk.fileName.startsWith("entrypoint")) {
if (chunk.type === "chunk" && chunk.isEntry) {
// Insert after the shebang line if present, otherwise at the top.
if (chunk.code.startsWith("#!")) {
const newline = chunk.code.indexOf("\n");
@@ -60,6 +60,13 @@ function injectBanner(): import("vite").Plugin {
};
}
const buildInputs: Record<string, string> = {
index: resolve(__dirname, "entrypoint.ts"),
};
if (process.env.LIVESYNC_CLI_TEST_SUPPORT === "1") {
buildInputs["p2p-lifecycle-test"] = resolve(__dirname, "test-support/p2p-lifecycle-entrypoint.ts");
}
export default defineConfig({
plugins: [svelte(), injectBanner()],
resolve: {
@@ -83,9 +90,7 @@ export default defineConfig({
emptyOutDir: true,
minify: false,
rollupOptions: {
input: {
index: resolve(__dirname, "entrypoint.ts"),
},
input: buildInputs,
external: (id) => {
if (isBuiltin(id)) return true;
if (defaultExternal.includes(id)) return true;
@@ -100,7 +105,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, "entrypoint.ts"),
formats: ["cjs"],
fileName: "index",
fileName: (_format, entryName) => `${entryName}.cjs`,
},
},
define: {