Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/npm-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,46 @@ jobs:
npm pack --dry-run 2>&1 | tee /tmp/npm-pack-dry-run.log
grep -q "dist/client/index.html" /tmp/npm-pack-dry-run.log

# Install the packed tarball the way a real `npx @threadlines/server` user
# would (fresh tree, registry-resolved transitive deps) and require it to
# boot. Catches what the tarball grep cannot: dependency skew like the
# 0.3.1 release, where a peer range resolved to a second copy of effect
# and the server exited 1 before printing anything.
- name: Smoke test packed artifact
shell: bash
run: |
set -euo pipefail
pack_dir="$GITHUB_WORKSPACE/release/npm-server"
tarball="$(cd "$pack_dir" && npm pack --silent)"
smoke_dir="$(mktemp -d)"
cd "$smoke_dir"
npm install --no-audit --no-fund --loglevel=error "$pack_dir/$tarball"
effect_copies="$(find node_modules -path '*node_modules/effect/package.json' | wc -l)"
if [ "$effect_copies" -ne 1 ]; then
echo "Expected exactly one installed copy of effect, found $effect_copies:"
find node_modules -path '*node_modules/effect/package.json'
exit 1
fi
node node_modules/@threadlines/server/dist/bin.mjs --port 7777 --no-browser --base-dir "$smoke_dir/home" > server.log 2>&1 &
server_pid=$!
for _ in $(seq 1 30); do
if grep -q "Listening on http" server.log; then
echo "Server booted."
kill "$server_pid" || true
exit 0
fi
if ! kill -0 "$server_pid" 2>/dev/null; then
echo "Server process exited before becoming ready. Log:"
cat server.log
exit 1
fi
sleep 1
done
echo "Server did not become ready within 30s. Log:"
cat server.log
kill "$server_pid" || true
exit 1

publish:
name: Publish package
needs: pack
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as NetService from "@threadlines/shared/Net";
import packageJson from "../package.json" with { type: "json" };
import { authCommand } from "./cli/auth.ts";
import { sharedServerCommandFlags } from "./cli/config.ts";
import { assertSingleEffectRuntime } from "./cli/effectRuntimeCheck.ts";
import { projectCommand } from "./cli/project.ts";
import { runServerCommand, serveCommand, startCommand } from "./cli/server.ts";

Expand All @@ -20,6 +21,7 @@ export const cli = Command.make("threadlines", { ...sharedServerCommandFlags }).
);

if (import.meta.main) {
assertSingleEffectRuntime();
Command.run(cli, { version: packageJson.version }).pipe(
Effect.scoped,
Effect.provide(CliRuntimeLayer),
Expand Down
58 changes: 58 additions & 0 deletions apps/server/src/cli/effectRuntimeCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { readFileSync, realpathSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";

function readVersion(packageJsonPath: string): string {
try {
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version?: string };
return parsed.version ?? "unknown";
} catch {
return "unknown";
}
}

/**
* Fails fast, before the Effect runtime spins up, when the install contains two
* copies of `effect` (e.g. an npm peer-range resolving to a newer release than
* our exact pin). A mixed install poisons Effect's own error reporting: each
* copy misclassifies the other's Cause objects, so the process exits 1 with no
* output at all instead of surfacing the underlying failure. This guard turns
* that silent death into an actionable message. Runs plain sync Node on
* purpose; it must not depend on the runtime it is validating.
*/
export function assertSingleEffectRuntime(): void {
let serverEffectPath: string;
let platformEffectPath: string;
try {
const requireFromServer = createRequire(import.meta.url);
serverEffectPath = realpathSync(requireFromServer.resolve("effect/package.json"));
const requireFromPlatform = createRequire(
requireFromServer.resolve("@effect/platform-node/package.json"),
);
platformEffectPath = realpathSync(requireFromPlatform.resolve("effect/package.json"));
} catch {
// Never block startup because the check itself could not resolve modules.
return;
}
if (serverEffectPath === platformEffectPath) return;

const serverDir = path.dirname(serverEffectPath);
const platformDir = path.dirname(platformEffectPath);
process.stderr.write(
[
'Threadlines failed to start: this install contains two conflicting copies of the "effect" runtime.',
"",
` server resolves: ${serverDir} (${readVersion(serverEffectPath)})`,
` platform resolves: ${platformDir} (${readVersion(platformEffectPath)})`,
"",
"This is a broken package installation, not a problem with your setup.",
"Reinstalling the latest version usually fixes it:",
"",
" npx -y @threadlines/server@latest",
"",
"If it persists, please report it: https://github.com/Threadlines/threadlines/issues",
"",
].join("\n"),
);
process.exit(1);
}
File renamed without changes.
Loading
Loading