Split self-host server package
This commit is contained in:
@@ -82,42 +82,18 @@ pnpm --filter happy run build
|
||||
|
||||
Report success/failure. Stop on failure.
|
||||
|
||||
### Step 5b: Rebuild bundled assets (server + webapp) — REQUIRED for every CLI release
|
||||
### Step 5b: Self-host server split
|
||||
|
||||
**Do not skip this. `pnpm run build` does NOT rebuild `tools/`.** The npm package
|
||||
ships `tools/` (see `files` in package.json) **as-is from disk**. The bundled
|
||||
self-host server (`tools/server/<plat>/`) and web app (`tools/webapp/`) are
|
||||
produced by separate scripts, so a plain build + publish ships **whatever stale
|
||||
bundle happened to be on disk** — this is exactly how a beta went out with an old
|
||||
server bundle and a broken Prisma engine.
|
||||
The `happy` npm package no longer bundles the self-host server binary or webapp.
|
||||
Packaged installs resolve those from the separately installed
|
||||
`happy-server-self-host` package. Do not rebuild or ship `tools/server` or
|
||||
`tools/webapp` as part of a CLI release.
|
||||
|
||||
```bash
|
||||
# 1. Regenerate Prisma client before compiling the server.
|
||||
# The native query engine is provided at install time by happy's
|
||||
# @prisma/engines dependency; do not package all platform engines into tools/.
|
||||
pnpm --filter happy-server generate
|
||||
|
||||
# 2. Cross-build the server binary for all 5 platforms (NOT host-only).
|
||||
pnpm --filter happy run bundle:server:all
|
||||
|
||||
# 3. Rebuild the bundled web app.
|
||||
pnpm --filter happy run bundle:webapp
|
||||
```
|
||||
|
||||
Sanity-check the output before continuing — every platform dir under
|
||||
`packages/happy-cli/tools/server/` must contain `happy-server`,
|
||||
`pglite.wasm`, `pglite.data`, `prisma/migrations/`, and `tools/webapp/index.html`
|
||||
must exist:
|
||||
|
||||
```bash
|
||||
ls packages/happy-cli/tools/server/*/ && ls packages/happy-cli/tools/webapp/index.html
|
||||
```
|
||||
|
||||
`bundle:server` needs `bun` on PATH. Cross-compiling all platforms from one host
|
||||
is supported by `bun build --compile --target`. The Prisma query engine is a
|
||||
native `.node` file and is NOT embeddable in the bun binary; `happy-cli` resolves
|
||||
it from its `@prisma/engines` dependency and points Prisma at it via
|
||||
`PRISMA_QUERY_ENGINE_LIBRARY`.
|
||||
If the CLI release depends on self-host server changes, release
|
||||
`happy-server-self-host` separately: regenerate Prisma, build the bundled webapp
|
||||
with `pnpm --filter happy-server-self-host run bundle:webapp`, then publish the
|
||||
server package. The server package is a JS/TS npm package; npm handles platform
|
||||
specific dependencies such as Prisma and sharp normally.
|
||||
|
||||
### Step 6: Test (unit only)
|
||||
|
||||
@@ -368,7 +344,7 @@ Separate repo, not part of this monorepo. Guide the user to push to that repo.
|
||||
- **Release notes: investigate with subagents, exclude default-off, ask when unsure** — see "Writing release notes" above.
|
||||
- **Always present options** — never assume which component, channel, or version.
|
||||
- **Always verify before publishing** — show the user what will be published and get confirmation.
|
||||
- **Always rebuild bundled assets on a CLI release** — `pnpm --filter happy-server generate`, then `bundle:server:all` + `bundle:webapp` before pack/publish (Step 5b). `pnpm run build` does NOT do this; the tarball ships `tools/` from disk, so skipping it ships a stale/broken server + webapp.
|
||||
- **Do not bundle self-host server/webapp into `happy`** — self-host runtime and the bundled webapp ship through `happy-server-self-host`, not the main CLI package.
|
||||
- **Unit tests are the gate, not integration tests** — integration tests are slow and have flaky abort/interrupt tests.
|
||||
- **Use pnpm publish, not npm publish** — avoids workspace protocol issues.
|
||||
- **Use --ignore-scripts** — we build and test explicitly, no need for prepublishOnly to redo it.
|
||||
|
||||
@@ -5,11 +5,13 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'packages/happy-cli/**'
|
||||
- 'packages/happy-server/**'
|
||||
- '.github/workflows/cli-smoke-test.yml'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'packages/happy-cli/**'
|
||||
- 'packages/happy-server/**'
|
||||
- '.github/workflows/cli-smoke-test.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -39,25 +41,24 @@ jobs:
|
||||
- name: Build package
|
||||
run: pnpm --filter happy build
|
||||
|
||||
# `happy server` ships a bun-compiled binary under tools/server/.
|
||||
# `pnpm build` does NOT produce it — without this step the packed tarball has
|
||||
# no server bundle and `happy server` cannot run. The Prisma engine is
|
||||
# resolved from happy's @prisma/engines dependency at install time.
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Generate Prisma client + bundle server (host platform)
|
||||
# `happy server` resolves the self-host runtime and bundled webapp from
|
||||
# happy-server-self-host. The server package is JS/TS and lets npm install
|
||||
# platform-specific dependencies such as Prisma and sharp normally.
|
||||
- name: Generate Prisma client + bundle happy-server-self-host webapp
|
||||
run: |
|
||||
pnpm --filter happy-server generate
|
||||
pnpm --filter happy run bundle:server
|
||||
pnpm --filter happy-server-self-host generate
|
||||
pnpm --filter happy-server-self-host run bundle:webapp
|
||||
|
||||
- name: Pack package
|
||||
run: pnpm --filter happy pack --pack-destination packages/happy-cli
|
||||
|
||||
- name: Install packed package globally
|
||||
- name: Pack packages
|
||||
run: |
|
||||
PACKAGE_FILE=$(ls packages/happy-cli/*.tgz)
|
||||
npm install -g "./$PACKAGE_FILE"
|
||||
pnpm --filter happy pack --pack-destination packages/happy-cli
|
||||
pnpm --filter happy-server-self-host pack --pack-destination packages/happy-server
|
||||
|
||||
- name: Install packed packages globally
|
||||
run: |
|
||||
HAPPY_PACKAGE_FILE=$(ls packages/happy-cli/*.tgz)
|
||||
SERVER_PACKAGE_FILE=$(ls packages/happy-server/*.tgz)
|
||||
npm install -g "./$SERVER_PACKAGE_FILE" "./$HAPPY_PACKAGE_FILE"
|
||||
|
||||
- name: Test binary execution
|
||||
run: |
|
||||
@@ -88,7 +89,7 @@ jobs:
|
||||
|
||||
echo "Binary smoke test passed on Linux!"
|
||||
|
||||
- name: Test bundled server (happy server)
|
||||
- name: Test packaged server (happy server)
|
||||
run: |
|
||||
export HAPPY_HOME_DIR="$(mktemp -d)"
|
||||
timeout 90s happy server --port 4505 --host 127.0.0.1 --no-persist --reset > /tmp/happy-server.log 2>&1 &
|
||||
@@ -103,7 +104,7 @@ jobs:
|
||||
BODY=$(curl -s -m 5 -G http://127.0.0.1:4505/v1/auth/request/status --data-urlencode "publicKey=$PK" || true)
|
||||
echo "auth/request/status -> $BODY"
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
pkill -f tools/server || true
|
||||
pkill -f happy-server || true
|
||||
echo "=== happy server log ==="; cat /tmp/happy-server.log || true
|
||||
if [ "$UP" != "1" ]; then echo "Error: happy server did not become healthy"; exit 1; fi
|
||||
if ! echo "$BODY" | grep -q '"status"'; then echo "Error: Prisma-backed endpoint did not respond"; exit 1; fi
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
"dist",
|
||||
"bin",
|
||||
"scripts",
|
||||
"tools",
|
||||
"tools/archives",
|
||||
"tools/licenses",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
@@ -77,7 +78,6 @@
|
||||
"@noble/ed25519": "^3.0.0",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@prisma/engines": "6.19.2",
|
||||
"@slopus/happy-wire": "workspace:*",
|
||||
"@stablelib/base64": "^2.0.1",
|
||||
"@stablelib/hex": "^2.0.1",
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Bundles happy-server into a self-contained artifact shipped inside happy-cli/tools/server/.
|
||||
* Bundles happy-server into a self-contained artifact directory.
|
||||
*
|
||||
* Uses `bun build --compile` to produce a single platform-specific binary, then copies the
|
||||
* pglite WASM/data files and prisma migrations alongside. happy-cli does NOT depend on the
|
||||
* happy-server workspace package — we reach into the sibling directory at build time only.
|
||||
*
|
||||
* Layout produced:
|
||||
* tools/server/
|
||||
* <out-dir>/
|
||||
* <platform>/
|
||||
* happy-server # bun-compiled binary
|
||||
* pglite.wasm # PGlite expects these next to process.execPath
|
||||
* pglite.data
|
||||
* prisma/migrations/...
|
||||
*
|
||||
* Prisma query engine: bun --compile cannot embed native .node modules.
|
||||
* happy-cli depends on @prisma/engines and points PRISMA_QUERY_ENGINE_LIBRARY
|
||||
* at the npm-installed engine for the current machine when spawning this binary.
|
||||
* Prisma query engine: bun --compile cannot embed native .node modules. The package
|
||||
* that runs this binary must provide @prisma/engines and point Prisma at the native
|
||||
* library for the current machine via PRISMA_QUERY_ENGINE_LIBRARY.
|
||||
*
|
||||
* Default: builds for the current host platform only. Pass --all-platforms to cross-build
|
||||
* for all six (used by release/CI).
|
||||
* for every supported platform (used by release/CI).
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
@@ -31,7 +31,6 @@ const { spawnSync } = require('node:child_process');
|
||||
const PACKAGE_DIR = path.resolve(__dirname, '..');
|
||||
const REPO_ROOT = path.resolve(PACKAGE_DIR, '..', '..');
|
||||
const SERVER_DIR = path.resolve(REPO_ROOT, 'packages/happy-server');
|
||||
const OUT_DIR = path.resolve(PACKAGE_DIR, 'tools/server');
|
||||
|
||||
const BUN_TARGETS = {
|
||||
'arm64-darwin': 'bun-darwin-arm64',
|
||||
@@ -78,13 +77,13 @@ function findPgliteAsset(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildPlatform(plat) {
|
||||
function buildPlatform(plat, outRoot) {
|
||||
if (!BUN_TARGETS[plat]) {
|
||||
console.error(`Unsupported platform: ${plat}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const target = BUN_TARGETS[plat];
|
||||
const outDir = path.join(OUT_DIR, plat);
|
||||
const outDir = path.join(outRoot, plat);
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const outFile = path.join(outDir, platformBinaryName(plat));
|
||||
|
||||
@@ -111,8 +110,8 @@ function buildPlatform(plat) {
|
||||
);
|
||||
}
|
||||
|
||||
function copyAssetsForPlatform(plat) {
|
||||
const platDir = path.join(OUT_DIR, plat);
|
||||
function copyAssetsForPlatform(plat, outRoot) {
|
||||
const platDir = path.join(outRoot, plat);
|
||||
console.log(`\n→ Copying assets (pglite + migrations) into ${path.relative(PACKAGE_DIR, platDir)}`);
|
||||
|
||||
for (const asset of ['pglite.wasm', 'pglite.data']) {
|
||||
@@ -135,22 +134,35 @@ function copyAssetsForPlatform(plat) {
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const allPlatforms = args.includes('--all-platforms');
|
||||
const outDirArg = valueAfter(args, '--out-dir');
|
||||
const outDir = outDirArg ? path.resolve(process.cwd(), outDirArg) : path.resolve(PACKAGE_DIR, 'tools/server');
|
||||
|
||||
if (!fs.existsSync(SERVER_DIR)) {
|
||||
console.error(`Missing ${SERVER_DIR}. Run from the monorepo.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
rmrf(OUT_DIR);
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
rmrf(outDir);
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const targets = allPlatforms ? Object.keys(BUN_TARGETS) : [currentPlatform()];
|
||||
for (const plat of targets) {
|
||||
buildPlatform(plat);
|
||||
copyAssetsForPlatform(plat);
|
||||
buildPlatform(plat, outDir);
|
||||
copyAssetsForPlatform(plat, outDir);
|
||||
}
|
||||
|
||||
console.log(`\n✓ happy-server bundle written to ${OUT_DIR}`);
|
||||
console.log(`\n✓ happy-server bundle written to ${outDir}`);
|
||||
}
|
||||
|
||||
function valueAfter(args, flag) {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx === -1) return null;
|
||||
const value = args[idx + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
console.error(`Missing value for ${flag}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
/**
|
||||
* Runs `expo export -p web` in packages/happy-app and copies the output into
|
||||
* happy-cli/tools/webapp/. happy-cli ships this directory so `happy server` can serve the
|
||||
* web client statically alongside the API.
|
||||
* a package-owned artifact directory. By default this writes to happy-cli/tools/webapp
|
||||
* for local development. Release packaging can pass --out-dir to place it elsewhere.
|
||||
*
|
||||
* happy-cli does NOT depend on happy-app — we reach into the sibling at build time only.
|
||||
*/
|
||||
@@ -16,7 +16,6 @@ const PACKAGE_DIR = path.resolve(__dirname, '..');
|
||||
const REPO_ROOT = path.resolve(PACKAGE_DIR, '..', '..');
|
||||
const APP_DIR = path.resolve(REPO_ROOT, 'packages/happy-app');
|
||||
const APP_DIST = path.join(APP_DIR, 'dist');
|
||||
const OUT_DIR = path.resolve(PACKAGE_DIR, 'tools/webapp');
|
||||
|
||||
function rmrf(p) {
|
||||
fs.rmSync(p, { recursive: true, force: true });
|
||||
@@ -31,6 +30,10 @@ function run(cmd, args, opts = {}) {
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const outDirArg = valueAfter(args, '--out-dir');
|
||||
const outDir = outDirArg ? path.resolve(process.cwd(), outDirArg) : path.resolve(PACKAGE_DIR, 'tools/webapp');
|
||||
|
||||
if (!fs.existsSync(APP_DIR)) {
|
||||
console.error(`Missing ${APP_DIR}. Run from the monorepo.`);
|
||||
process.exit(1);
|
||||
@@ -45,12 +48,23 @@ function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n→ Copying webapp into ${OUT_DIR}`);
|
||||
rmrf(OUT_DIR);
|
||||
fs.mkdirSync(path.dirname(OUT_DIR), { recursive: true });
|
||||
fs.cpSync(APP_DIST, OUT_DIR, { recursive: true });
|
||||
console.log(`\n→ Copying webapp into ${outDir}`);
|
||||
rmrf(outDir);
|
||||
fs.mkdirSync(path.dirname(outDir), { recursive: true });
|
||||
fs.cpSync(APP_DIST, outDir, { recursive: true });
|
||||
|
||||
console.log(`\n✓ webapp written to ${OUT_DIR}`);
|
||||
console.log(`\n✓ webapp written to ${outDir}`);
|
||||
}
|
||||
|
||||
function valueAfter(args, flag) {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx === -1) return null;
|
||||
const value = args[idx + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
console.error(`Missing value for ${flag}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { createInterface } from 'node:readline/promises';
|
||||
import { configuration } from '@/configuration';
|
||||
import { updateSettings } from '@/persistence';
|
||||
|
||||
@@ -19,12 +20,15 @@ const PRISMA_QUERY_ENGINE_FILES: Record<string, string> = {
|
||||
'x64-linux': 'libquery_engine-debian-openssl-3.0.x.so.node',
|
||||
'x64-win32': 'query_engine-windows.dll.node',
|
||||
};
|
||||
const SERVER_PACKAGE_NAME = 'happy-server-self-host';
|
||||
const SETTINGS_WRITE_CONFIRM_FLAG = '--i-understand-this-will-modify-default-happy-settings';
|
||||
|
||||
interface ServerOptions {
|
||||
port: number;
|
||||
host: string;
|
||||
reset: boolean;
|
||||
persistServerUrl: boolean;
|
||||
allowSettingsWrite: boolean;
|
||||
masterSecret?: string;
|
||||
}
|
||||
|
||||
@@ -37,12 +41,31 @@ interface ServerArtifacts {
|
||||
cwd: string;
|
||||
/** True when running the bundled bun binary; false when running from monorepo source. */
|
||||
bundled: boolean;
|
||||
/** Where this runnable came from. */
|
||||
source: 'package' | 'legacy-bundled' | 'source';
|
||||
/** Prisma's native query engine path for bun-compiled server binaries. */
|
||||
prismaQueryEngineLibrary?: string;
|
||||
/** Static web app directory served by the self-host server. */
|
||||
webappDir?: string;
|
||||
}
|
||||
|
||||
interface HappyServerPackageArtifact {
|
||||
command: string;
|
||||
prefixArgs?: string[];
|
||||
cwd: string;
|
||||
bundled?: boolean;
|
||||
source?: string;
|
||||
prismaQueryEngineLibrary?: string;
|
||||
webappDir?: string;
|
||||
}
|
||||
|
||||
export async function handleServerCommand(args: string[]): Promise<void> {
|
||||
const opts = parseArgs(args);
|
||||
if (opts === null) return;
|
||||
|
||||
const serverUrl = `http://${opts.host === '0.0.0.0' ? '127.0.0.1' : opts.host}:${opts.port}`;
|
||||
await ensureSettingsWriteAllowed(opts, serverUrl);
|
||||
|
||||
const dataDir = path.join(configuration.happyHomeDir, 'server-data');
|
||||
const pgliteDir = path.join(dataDir, 'pglite');
|
||||
const secretFile = path.join(dataDir, 'master-secret');
|
||||
@@ -58,20 +81,21 @@ export async function handleServerCommand(args: string[]): Promise<void> {
|
||||
|
||||
const artifacts = resolveServerArtifacts();
|
||||
if (!artifacts) {
|
||||
console.error(chalk.red('Could not locate the happy-server bundle or source.'));
|
||||
console.error(chalk.red('Could not locate happy-server.'));
|
||||
console.error(chalk.gray(' Expected one of:'));
|
||||
console.error(chalk.gray(` - bundled binary at ${path.join(__dirname, '..', '..', 'tools', 'server', currentPlatform(), bundledBinaryName())}`));
|
||||
console.error(chalk.gray(` - installed ${SERVER_PACKAGE_NAME} package`));
|
||||
console.error(chalk.gray(` - legacy bundled binary at ${path.join(__dirname, '..', '..', 'tools', 'server', currentPlatform(), bundledBinaryName())}`));
|
||||
console.error(chalk.gray(' - sibling packages/happy-server/sources/standalone.ts in the monorepo'));
|
||||
console.error(chalk.gray(` For npm installs, run: npm install -g ${SERVER_PACKAGE_NAME}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const serverUrl = `http://${opts.host === '0.0.0.0' ? '127.0.0.1' : opts.host}:${opts.port}`;
|
||||
const staticDir = findWebappDir();
|
||||
const staticDir = artifacts.webappDir ?? findWebappDir();
|
||||
|
||||
console.log(chalk.cyan(`\n happy server`));
|
||||
console.log(chalk.gray(` data dir: ${dataDir}`));
|
||||
console.log(chalk.gray(` server url: ${serverUrl}`));
|
||||
console.log(chalk.gray(` mode: ${artifacts.bundled ? 'bundled' : 'source (dev)'}`));
|
||||
console.log(chalk.gray(` mode: ${serverArtifactMode(artifacts)}`));
|
||||
if (staticDir) {
|
||||
console.log(chalk.gray(` webapp: ${staticDir}`));
|
||||
} else {
|
||||
@@ -98,11 +122,16 @@ export async function handleServerCommand(args: string[]): Promise<void> {
|
||||
// mode resolves the engine from node_modules normally, but bundled mode needs
|
||||
// an explicit path because bun's bunfs execPath defeats Prisma's search.
|
||||
if (artifacts.bundled) {
|
||||
const prismaEngine = resolvePrismaQueryEngineLibrary(artifacts.cwd);
|
||||
const prismaEngine = artifacts.prismaQueryEngineLibrary ?? resolvePrismaQueryEngineLibrary(artifacts.cwd);
|
||||
if (!prismaEngine) {
|
||||
console.error(chalk.red('Could not locate the Prisma query engine for this platform.'));
|
||||
console.error(chalk.gray(' Expected @prisma/engines to be installed with the happy package.'));
|
||||
console.error(chalk.gray(' Try reinstalling happy, then run `happy server` again.'));
|
||||
if (artifacts.source === 'package') {
|
||||
console.error(chalk.gray(` Expected ${SERVER_PACKAGE_NAME} to install @prisma/engines.`));
|
||||
console.error(chalk.gray(` Try reinstalling ${SERVER_PACKAGE_NAME}, then run \`happy server\` again.`));
|
||||
} else {
|
||||
console.error(chalk.gray(' Expected @prisma/engines to be available near the happy package.'));
|
||||
console.error(chalk.gray(' Try reinstalling happy, then run `happy server` again.'));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
env.PRISMA_QUERY_ENGINE_LIBRARY = prismaEngine;
|
||||
@@ -138,13 +167,13 @@ export async function handleServerCommand(args: string[]): Promise<void> {
|
||||
process.on('SIGINT', () => forwardSignal('SIGINT'));
|
||||
process.on('SIGTERM', () => forwardSignal('SIGTERM'));
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
const exitCode = await new Promise<number>(resolve => {
|
||||
child.on('exit', code => {
|
||||
console.log(chalk.gray(`\nhappy-server exited (code ${code ?? 0})`));
|
||||
resolve();
|
||||
resolve(code ?? 0);
|
||||
});
|
||||
});
|
||||
process.exit(0);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
function parseArgs(args: string[]): ServerOptions | null {
|
||||
@@ -152,6 +181,7 @@ function parseArgs(args: string[]): ServerOptions | null {
|
||||
let host = '127.0.0.1';
|
||||
let reset = false;
|
||||
let persistServerUrl = true;
|
||||
let allowSettingsWrite = false;
|
||||
let masterSecret: string | undefined;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
@@ -171,6 +201,8 @@ function parseArgs(args: string[]): ServerOptions | null {
|
||||
reset = true;
|
||||
} else if (arg === '--no-persist') {
|
||||
persistServerUrl = false;
|
||||
} else if (arg === SETTINGS_WRITE_CONFIRM_FLAG) {
|
||||
allowSettingsWrite = true;
|
||||
} else if (arg === '--master-secret') {
|
||||
masterSecret = args[++i];
|
||||
} else {
|
||||
@@ -180,7 +212,7 @@ function parseArgs(args: string[]): ServerOptions | null {
|
||||
}
|
||||
}
|
||||
|
||||
return { port, host, reset, persistServerUrl, masterSecret };
|
||||
return { port, host, reset, persistServerUrl, allowSettingsWrite, masterSecret };
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
@@ -195,15 +227,48 @@ ${chalk.bold('Options:')}
|
||||
--host <ip> Host to bind (default: 127.0.0.1)
|
||||
--reset Wipe local server data before starting
|
||||
--no-persist Don't write serverUrl into settings.json
|
||||
${SETTINGS_WRITE_CONFIRM_FLAG}
|
||||
Write settings.serverUrl/settings.webappUrl without prompting
|
||||
--master-secret <hex> Use a specific master secret (default: auto-generated)
|
||||
|
||||
${chalk.bold('Notes:')}
|
||||
- Stores data in ${chalk.cyan('$HAPPY_HOME_DIR/server-data/')}
|
||||
- Writes ${chalk.cyan('settings.serverUrl')} so happy CLI + daemon point at it automatically
|
||||
- Packaged installs require ${chalk.cyan(SERVER_PACKAGE_NAME)} for the local server binary
|
||||
- By default, asks before writing ${chalk.cyan('settings.serverUrl')} and ${chalk.cyan('settings.webappUrl')}
|
||||
- Use ${chalk.cyan('--no-persist')} to run without modifying default Happy settings
|
||||
- Open ${chalk.cyan('http://127.0.0.1:<port>')} for the web app (if bundled)
|
||||
`);
|
||||
}
|
||||
|
||||
async function ensureSettingsWriteAllowed(opts: ServerOptions, serverUrl: string): Promise<void> {
|
||||
if (!opts.persistServerUrl || opts.allowSettingsWrite) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
`happy server will write settings.serverUrl and settings.webappUrl to ${serverUrl} ` +
|
||||
`in ${configuration.settingsFile}.`;
|
||||
|
||||
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
||||
console.error(chalk.red('Refusing to modify default Happy settings from a non-interactive run.'));
|
||||
console.error(chalk.gray(message));
|
||||
console.error(chalk.gray(`Re-run with --no-persist, or pass ${SETTINGS_WRITE_CONFIRM_FLAG}.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
||||
try {
|
||||
const answer = await rl.question(`${chalk.yellow(message)} Continue? ${chalk.gray('[y/N]')} `);
|
||||
const normalized = answer.trim().toLowerCase();
|
||||
if (normalized !== 'y' && normalized !== 'yes') {
|
||||
console.error(chalk.gray('Cancelled. Re-run with --no-persist to start without changing settings.'));
|
||||
process.exit(1);
|
||||
}
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
function loadOrCreateMasterSecret(file: string): string {
|
||||
if (existsSync(file)) {
|
||||
return readFileSync(file, 'utf8').trim();
|
||||
@@ -280,20 +345,30 @@ function ensureExecutable(file: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function serverArtifactMode(artifacts: ServerArtifacts): string {
|
||||
if (artifacts.source === 'package') return SERVER_PACKAGE_NAME;
|
||||
if (artifacts.source === 'legacy-bundled') return 'legacy bundled';
|
||||
return 'source (dev)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the artifacts needed to spawn happy-server.
|
||||
*
|
||||
* Order:
|
||||
* 1. Bundled binary at tools/server/<platform>/happy-server (shipped with npm package)
|
||||
* 2. Source-mode fallback for monorepo dev: ../happy-server/sources/standalone.ts via tsx
|
||||
* 1. happy-server-self-host package (npm-installed local server artifact)
|
||||
* 2. Legacy bundled binary at tools/server/<platform>/happy-server
|
||||
* 3. Source-mode fallback for monorepo dev: ../happy-server/sources/standalone.ts via tsx
|
||||
*/
|
||||
function resolveServerArtifacts(): ServerArtifacts | undefined {
|
||||
const packageArtifact = resolveInstalledServerPackage();
|
||||
if (packageArtifact) return packageArtifact;
|
||||
|
||||
const toolsRoot = resolveToolsPath('server');
|
||||
const binDir = path.join(toolsRoot, currentPlatform());
|
||||
const binary = path.join(binDir, bundledBinaryName());
|
||||
if (existsSync(binary)) {
|
||||
ensureExecutable(binary);
|
||||
return { command: binary, prefixArgs: [], cwd: binDir, bundled: true };
|
||||
return { command: binary, prefixArgs: [], cwd: binDir, bundled: true, source: 'legacy-bundled' };
|
||||
}
|
||||
|
||||
const sourceEntry = findSourceStandalone();
|
||||
@@ -305,12 +380,36 @@ function resolveServerArtifacts(): ServerArtifacts | undefined {
|
||||
prefixArgs: useNode ? [tsx, sourceEntry] : [sourceEntry],
|
||||
cwd: path.dirname(path.dirname(sourceEntry)),
|
||||
bundled: false,
|
||||
source: 'source',
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveInstalledServerPackage(): ServerArtifacts | undefined {
|
||||
try {
|
||||
const serverPackage = require_(SERVER_PACKAGE_NAME) as {
|
||||
resolveServerArtifact?: () => HappyServerPackageArtifact | undefined;
|
||||
};
|
||||
const artifact = serverPackage.resolveServerArtifact?.();
|
||||
if (!artifact || !artifact.command || !existsSync(artifact.command)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
command: artifact.command,
|
||||
prefixArgs: artifact.prefixArgs ?? [],
|
||||
cwd: artifact.cwd,
|
||||
bundled: artifact.bundled ?? true,
|
||||
source: 'package',
|
||||
prismaQueryEngineLibrary: artifact.prismaQueryEngineLibrary,
|
||||
webappDir: artifact.webappDir,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function findSourceStandalone(): string | undefined {
|
||||
const candidates = [
|
||||
path.resolve(__dirname, '../../../happy-server/sources/standalone.ts'),
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
node_modules
|
||||
.env
|
||||
dist
|
||||
.pgdata
|
||||
.minio
|
||||
|
||||
.env.local
|
||||
.env
|
||||
|
||||
.logs/
|
||||
|
||||
.claude/
|
||||
|
||||
# Standalone build artifacts
|
||||
webapp/
|
||||
data/
|
||||
happy-server
|
||||
pglite.wasm
|
||||
pglite.data
|
||||
.logs/
|
||||
*.tgz
|
||||
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { spawn } = require('node:child_process');
|
||||
const { resolveServerArtifact } = require('../index.cjs');
|
||||
|
||||
const artifact = resolveServerArtifact();
|
||||
if (!artifact) {
|
||||
console.error('Could not locate the Happy server package runtime.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const env = { ...process.env };
|
||||
if (artifact.webappDir && !env.HAPPY_STATIC_DIR) {
|
||||
env.HAPPY_STATIC_DIR = artifact.webappDir;
|
||||
}
|
||||
|
||||
const child = spawn(artifact.command, [...artifact.prefixArgs, ...process.argv.slice(2)], {
|
||||
cwd: artifact.cwd,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
child.on('error', error => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) process.kill(process.pid, signal);
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { createRequire } = require('node:module');
|
||||
|
||||
const require_ = createRequire(__filename);
|
||||
|
||||
function packageRoot() {
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
function getWebappDirectory() {
|
||||
return path.join(packageRoot(), 'webapp');
|
||||
}
|
||||
|
||||
function findTsxCli() {
|
||||
return require_.resolve('tsx/cli', { paths: [packageRoot()] });
|
||||
}
|
||||
|
||||
function resolveServerArtifact() {
|
||||
const runtime = path.join(packageRoot(), 'dist', 'standalone.mjs');
|
||||
if (fs.existsSync(runtime)) {
|
||||
const webappDir = getWebappDirectory();
|
||||
return {
|
||||
command: process.execPath,
|
||||
prefixArgs: [runtime],
|
||||
cwd: packageRoot(),
|
||||
bundled: false,
|
||||
source: 'package',
|
||||
platform: `${process.arch}-${process.platform}`,
|
||||
webappDir: fs.existsSync(path.join(webappDir, 'index.html')) ? webappDir : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const standalone = path.join(packageRoot(), 'sources', 'standalone.ts');
|
||||
if (!fs.existsSync(standalone)) return undefined;
|
||||
|
||||
const webappDir = getWebappDirectory();
|
||||
return {
|
||||
command: process.execPath,
|
||||
prefixArgs: [findTsxCli(), standalone],
|
||||
cwd: packageRoot(),
|
||||
bundled: false,
|
||||
source: 'package',
|
||||
platform: `${process.arch}-${process.platform}`,
|
||||
webappDir: fs.existsSync(path.join(webappDir, 'index.html')) ? webappDir : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
packageRoot,
|
||||
getWebappDirectory,
|
||||
resolveServerArtifact,
|
||||
};
|
||||
@@ -1,18 +1,38 @@
|
||||
{
|
||||
"name": "happy-server",
|
||||
"version": "0.0.0",
|
||||
"repository": "https://github.com/slopus/happy-server.git",
|
||||
"author": "Steve Korshakov <steve@korshakov.com>",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./sources/index.ts",
|
||||
"exports": {
|
||||
".": "./sources/index.ts",
|
||||
"./standalone": "./sources/standalone.ts"
|
||||
"name": "happy-server-self-host",
|
||||
"version": "1.1.10",
|
||||
"description": "Happy self-host server and bundled web app",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/slopus/happy"
|
||||
},
|
||||
"homepage": "https://happy.engineering",
|
||||
"bugs": "https://github.com/slopus/happy/issues",
|
||||
"author": "Kirill Dubovitskiy",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"happy-server": "./bin/happy-server.cjs"
|
||||
},
|
||||
"main": "./index.cjs",
|
||||
"exports": {
|
||||
".": "./index.cjs",
|
||||
"./standalone": "./dist/standalone.mjs",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"index.cjs",
|
||||
"dist",
|
||||
"prisma",
|
||||
"webapp",
|
||||
"package.json",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"build": "tsc --noEmit && node scripts/build-runtime.cjs",
|
||||
"build:runtime": "node scripts/build-runtime.cjs",
|
||||
"bundle:webapp": "node ../happy-cli/scripts/bundle-webapp.cjs --out-dir webapp",
|
||||
"build:standalone": "bun build ./sources/standalone.ts --compile --outfile dist/happy-server --target bun && find ../../node_modules/@electric-sql/pglite/dist -name 'pglite.wasm' -exec cp {} dist/ \\; && find ../../node_modules/@electric-sql/pglite/dist -name 'pglite.data' -exec cp {} dist/ \\; && cp -r prisma/migrations dist/prisma/migrations",
|
||||
"start": "tsx ./sources/main.ts",
|
||||
"standalone": "tsx ./sources/standalone.ts",
|
||||
@@ -21,8 +41,8 @@
|
||||
"test": "vitest run",
|
||||
"migrate": "dotenv -e .env.dev -- prisma migrate dev",
|
||||
"migrate:reset": "dotenv -e .env.dev -- prisma migrate reset",
|
||||
"generate": "prisma generate",
|
||||
"postinstall": "prisma generate",
|
||||
"generate": "prisma generate --schema=prisma/schema.prisma",
|
||||
"postinstall": "prisma generate --schema=prisma/schema.prisma",
|
||||
"db": "docker run -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=handy -v $(pwd)/.pgdata:/var/lib/postgresql/data -p 5432:5432 postgres",
|
||||
"redis": "docker run -d -p 6379:6379 redis",
|
||||
"s3": "docker run -d --name minio -p 9000:9000 -p 9001:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin -v $(pwd)/.minio/data:/data minio/minio server /data --console-address :9001",
|
||||
@@ -32,12 +52,17 @@
|
||||
"devDependencies": {
|
||||
"@types/chalk": "^2.2.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^20.12.3",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/tmp": "^0.2.6",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "5.9.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.0",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -46,11 +71,9 @@
|
||||
"@fastify/bearer-auth": "^10.1.1",
|
||||
"@fastify/cors": "^10.0.1",
|
||||
"@fastify/static": "^8.1.1",
|
||||
"@prisma/client": "^6.11.1",
|
||||
"@prisma/client": "6.19.2",
|
||||
"@slopus/happy-wire": "workspace:*",
|
||||
"@socket.io/redis-streams-adapter": "^0.2.2",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/semver": "^7.7.0",
|
||||
"axios": "^1.6.8",
|
||||
"chalk": "4.1.2",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -65,7 +88,7 @@
|
||||
"pglite-prisma-adapter": "^0.7.2",
|
||||
"pino": "^10.3.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"prisma": "^6.11.1",
|
||||
"prisma": "6.19.2",
|
||||
"prisma-json-types-generator": "^3.5.1",
|
||||
"privacy-kit": "^0.0.25",
|
||||
"prom-client": "^15.1.3",
|
||||
@@ -75,12 +98,13 @@
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-adapter": "^2.5.5",
|
||||
"tmp": "^0.2.3",
|
||||
"tsx": "^4.19.2",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.0",
|
||||
"zod": "3.25.76",
|
||||
"zod-to-json-schema": "^3.24.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const pkg = require(path.join(root, 'package.json'));
|
||||
const dist = path.join(root, 'dist');
|
||||
|
||||
fs.rmSync(dist, { recursive: true, force: true });
|
||||
fs.mkdirSync(dist, { recursive: true });
|
||||
|
||||
const args = [
|
||||
'build',
|
||||
'./sources/standalone.ts',
|
||||
'--target',
|
||||
'node',
|
||||
'--format',
|
||||
'esm',
|
||||
'--outfile',
|
||||
'dist/standalone.mjs',
|
||||
];
|
||||
|
||||
const bundledDependencies = new Set([
|
||||
// The published 0.1.0 package does not include the newest voice schemas yet.
|
||||
// Keep the server release unblocked by bundling the workspace copy.
|
||||
'@slopus/happy-wire',
|
||||
]);
|
||||
|
||||
for (const dependency of Object.keys(pkg.dependencies ?? {})) {
|
||||
if (bundledDependencies.has(dependency)) continue;
|
||||
args.push('--external', dependency);
|
||||
}
|
||||
|
||||
const result = spawnSync('bun', args, {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
@@ -120,7 +120,7 @@ async function serve() {
|
||||
|
||||
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3005;
|
||||
const host = process.env.HOST || "0.0.0.0";
|
||||
const staticDir = process.env.HAPPY_STATIC_DIR || undefined;
|
||||
const staticDir = findStaticDir();
|
||||
let injectHtmlConfig: Record<string, unknown> | undefined;
|
||||
if (process.env.HAPPY_INJECT_HTML_CONFIG) {
|
||||
try {
|
||||
@@ -143,6 +143,23 @@ async function serve() {
|
||||
// Block until shutdown so the process stays alive.
|
||||
const { awaitShutdown } = await import("./utils/shutdown");
|
||||
await awaitShutdown();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function findStaticDir(): string | undefined {
|
||||
const candidates = [
|
||||
process.env.HAPPY_STATIC_DIR,
|
||||
path.join(process.cwd(), "webapp"),
|
||||
path.join(path.dirname(process.execPath), "webapp"),
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(path.join(candidate, "index.html"))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// CLI — only when this file is invoked directly, not when imported as a library.
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0"
|
||||
|
||||
Generated
+17
-20
@@ -927,9 +927,6 @@ importers:
|
||||
'@paralleldrive/cuid2':
|
||||
specifier: ^2.2.2
|
||||
version: 2.3.1
|
||||
'@prisma/engines':
|
||||
specifier: 6.19.2
|
||||
version: 6.19.2
|
||||
'@slopus/happy-wire':
|
||||
specifier: workspace:*
|
||||
version: link:../happy-wire
|
||||
@@ -1079,7 +1076,7 @@ importers:
|
||||
specifier: ^8.1.1
|
||||
version: 8.3.0
|
||||
'@prisma/client':
|
||||
specifier: ^6.11.1
|
||||
specifier: 6.19.2
|
||||
version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)
|
||||
'@slopus/happy-wire':
|
||||
specifier: workspace:*
|
||||
@@ -1087,12 +1084,6 @@ importers:
|
||||
'@socket.io/redis-streams-adapter':
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.3(socket.io-adapter@2.5.6)
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.10
|
||||
version: 9.0.10
|
||||
'@types/semver':
|
||||
specifier: ^7.7.0
|
||||
version: 7.7.1
|
||||
axios:
|
||||
specifier: ^1.6.8
|
||||
version: 1.13.4
|
||||
@@ -1136,7 +1127,7 @@ importers:
|
||||
specifier: ^13.0.0
|
||||
version: 13.1.3
|
||||
prisma:
|
||||
specifier: ^6.11.1
|
||||
specifier: 6.19.2
|
||||
version: 6.19.2(typescript@5.9.3)
|
||||
prisma-json-types-generator:
|
||||
specifier: ^3.5.1
|
||||
@@ -1165,21 +1156,12 @@ importers:
|
||||
tmp:
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.5
|
||||
tsx:
|
||||
specifier: ^4.19.2
|
||||
version: 4.21.0
|
||||
tweetnacl:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
uuid:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^5.1.4
|
||||
version: 5.1.4(typescript@5.9.3)(vite@8.0.9(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vitest:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
zod:
|
||||
specifier: 3.25.76
|
||||
version: 3.25.76
|
||||
@@ -1193,9 +1175,15 @@ importers:
|
||||
'@types/express':
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.25
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.10
|
||||
version: 9.0.10
|
||||
'@types/node':
|
||||
specifier: ^20.12.3
|
||||
version: 20.19.39
|
||||
'@types/semver':
|
||||
specifier: ^7.7.0
|
||||
version: 7.7.1
|
||||
'@types/tmp':
|
||||
specifier: ^0.2.6
|
||||
version: 0.2.6
|
||||
@@ -1208,9 +1196,18 @@ importers:
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@20.19.39)(typescript@5.9.3)
|
||||
tsx:
|
||||
specifier: ^4.20.6
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^5.1.4
|
||||
version: 5.1.4(typescript@5.9.3)(vite@8.0.9(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vitest:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
yaml:
|
||||
specifier: ^2.4.2
|
||||
version: 2.8.2
|
||||
|
||||
Reference in New Issue
Block a user