fix(rde): graceful config load errors + lightweight /config import path (#1557)

## Problem

A user hit `Failed to register development environment session (500)`
when running the RDE (`hexclave dev` / `stack dev`). Removing
`defineStackConfig` from their `stack.config.ts` made it go away.

**Root cause:** the local dashboard evaluates the project's config file
in a plain Node context via `jiti`
([config-file.ts](apps/dashboard/src/lib/remote-development-environment/config-file.ts)).
When the config imports a *value* (e.g. `defineStackConfig`) from a
framework package like `@stackframe/stack` / `@hexclave/next`, jiti
executes the entire SDK — React, `server-only`, Next internals — which
throws in that context. The exception propagated as a bare 500. Dropping
`defineStackConfig` removed the value import, so jiti no longer loaded
the framework.

## Changes

**1. Graceful error (Fix 3)**
`readConfigFile` now wraps the `jiti.import` in try/catch and rethrows a
message pointing at the lightweight import path, instead of a raw 500.

**2. Lightweight `/config` subpath (Fix 1)**
Added a side-effect-free `./config` entrypoint to the framework packages
— `@hexclave/{js,next,react,tanstack-start}/config` — that re-exports
`defineHexclaveConfig` / `defineStackConfig` + the `HexclaveConfig` type
from `@hexclave/shared/config`, with **no framework runtime**. Source of
truth:
[`packages/template/src/config.ts`](packages/template/src/config.ts) +
the export in
[`package-template.json`](packages/template/package-template.json),
propagated to the generated packages via `generate-sdks`.

> Why per-package and not `@hexclave/shared/config`: `@hexclave/shared`
is only a *transitive* dependency from a user's perspective, so
importing from it fails under pnpm strict mode. Users depend on the
framework package directly, so `@hexclave/next/config` always resolves.
This was confirmed empirically — the previous tests that imported
`@hexclave/shared/config` were red.

**3. Docs / prompts / renderer aligned to the new path**
-
[`ai-setup-prompt.ts`](packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts)
+ regenerated `docs-mintlify` (setup.mdx, llms-full.txt, snippets).
- Hand-written
[`hexclave-config.mdx`](docs-mintlify/guides/going-further/hexclave-config.mdx)
and
[`local-vs-cloud-dashboard.mdx`](docs-mintlify/guides/going-further/local-vs-cloud-dashboard.mdx).
(`docs/**` left untouched — legacy.)
- `renderConfigFileContent` (the config file the dashboard/CLI
auto-writes) now emits `import type { HexclaveConfig } from
"<pkg>/config"`. Legacy `@stackframe/*` packages predate the subpath, so
they keep their root import (guarded).

## Behavioral note

Existing config files that import from a package root get their import
line upgraded to `/config` on their next dashboard/CLI sync — a
one-time, harmless rewrite that migrates them onto the safe path. The
github-config-push idempotence test was updated to use the current
`/config` format so it still genuinely verifies "no spurious commit."

## Testing

- 43 unit tests pass across `config-file`, `github-config-push`,
`config-rendering`, `config-authoring`, `local-emulator`. The two
previously-red RDE `define*` tests now pass through jiti via
`@hexclave/next/config` (the real code path), and were made
resolution-stable by rooting their temp dir at the test file instead of
`process.cwd()`.
- Typecheck green on all source-changed packages (shared, cli, js, next,
react, tanstack-start). Lint clean.
- ⚠️ The two e2e suites (`cli.test.ts`, `config-local-emulator.test.ts`)
need backend+DB infra; their snapshot updates are mechanical and
**confirmable only in CI**.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Prevents 500s when loading `hexclave.config.ts` by adding a lightweight
`<pkg>/config` entrypoint and showing a clear, actionable error without
leaking framework stacks. Import detection, rendering, CLI, tests, and
docs now default to `/config` (including `@hexclave/tanstack-start`) so
configs load in plain Node contexts.

- **New Features**
- Added `/config` subpaths in `@hexclave/js`, `@hexclave/next`,
`@hexclave/react`, `@hexclave/tanstack-start` (and template)
re-exporting `defineHexclaveConfig`, `defineStackConfig`, and
`HexclaveConfig` with no framework runtime.
- Renderer, CLI, and docs import `HexclaveConfig` from `<pkg>/config`;
legacy `@stackframe/*` keep root imports. Existing config files
auto-upgrade on next dashboard/CLI sync.

- **Bug Fixes**
- Wrapped `jiti` config load with try/catch; capture raw error for
diagnostics and show a concise message pointing to `<pkg>/config` (no
nested framework stack traces).
- Import detection accepts optional `/config` suffix; renderer always
appends `/config` for Hexclave packages and recognizes
`@hexclave/tanstack-start`.
- Tests stabilized by scoping temp dirs to the test file; CLI error
example now references `HexclaveConfig` from `<pkg>/config` for Hexclave
packages.

<sup>Written for commit dfe7d5fee4.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1557?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added lightweight "/config" subpath exports across SDK packages to
enable side-effect-free config authoring in plain Node contexts.

* **Documentation**
* Updated guides and snippets to recommend importing config types and
helpers from the "/config" entrypoint and added example usage.

* **Bug Fixes**
* Improved error messaging when dynamic config imports fail, with
guidance to use the "/config" entrypoint.

* **Tests**
* Adjusted tests and snapshots to expect normalized "/config" import
paths.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
BilalG1 2026-06-10 11:40:19 -07:00 committed by GitHub
parent 88ef2ce85f
commit 7f99f15b42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 218 additions and 43 deletions

View File

@ -96,7 +96,7 @@ describe("local emulator config", () => {
await writeConfigToFile(absoluteFilePath, { auth: { allowLocalhost: true } });
await expect(fs.readFile(mountedFilePath, "utf-8")).resolves.toBe(
`import type { HexclaveConfig } from "@hexclave/js";\n\nexport const config: HexclaveConfig = {\n "auth": {\n "allowLocalhost": true\n }\n};\n`
`import type { HexclaveConfig } from "@hexclave/js/config";\n\nexport const config: HexclaveConfig = {\n "auth": {\n "allowLocalhost": true\n }\n};\n`
);
});

View File

@ -50,7 +50,7 @@ export const config: HexclaveConfig = {
`;
const result = buildUpdatedConfigFileContent(current, { "teams.allowClientTeamCreation": true });
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/next";
"import type { HexclaveConfig } from "@hexclave/next/config";
export const config: HexclaveConfig = {
"teams": {
@ -68,7 +68,7 @@ export const config: HexclaveConfig = {};
`;
const result = buildUpdatedConfigFileContent(current, { "auth.allowSignUp": true });
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/react";
"import type { HexclaveConfig } from "@hexclave/react/config";
export const config: HexclaveConfig = {
"auth": {
@ -104,7 +104,7 @@ export const config: StackConfig = {};
const current = `export const config = {};\n`;
const result = buildUpdatedConfigFileContent(current, { "auth.allowSignUp": true });
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"auth": {
@ -124,7 +124,7 @@ export const config: HexclaveConfig = {};
"payments.items.todos.customerType": "user",
});
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"payments": {
@ -150,7 +150,7 @@ export const config: HexclaveConfig = {
"payments.items.todos.displayName": "New",
});
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"payments": {
@ -226,7 +226,7 @@ export const config: HexclaveConfig = { teams: { allowClientTeamCreation: false
{
"body": {
"branch": "main",
"content": "import type { HexclaveConfig } from "@hexclave/js";
"content": "import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"teams": {
@ -266,7 +266,7 @@ export const config: HexclaveConfig = { teams: { allowClientTeamCreation: false
{
"body": {
"branch": "main",
"content": "import type { HexclaveConfig } from "@hexclave/js";
"content": "import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"auth": {
@ -288,7 +288,7 @@ export const config: HexclaveConfig = { teams: { allowClientTeamCreation: false
});
it("skips the commit when the new rendered file is identical to the old one", async () => {
const same = `import type { HexclaveConfig } from "@hexclave/js";
const same = `import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"teams": {

View File

@ -28,10 +28,13 @@ import {
*/
function detectImportPackage(currentFileContent: string): string | undefined {
// Match `from "@hexclave/<name>"` or `from "@stackframe/<name>"` — single
// or double quotes. Hexclave preferred when both appear.
const hexclave = currentFileContent.match(/from\s+["']@hexclave\/([a-z0-9-]+)["']/i);
// or double quotes, with an optional `/config` subpath suffix (the lightweight
// entrypoint newer config files import from). We return the bare package name;
// the renderer re-appends `/config` for Hexclave packages. Hexclave preferred
// when both appear.
const hexclave = currentFileContent.match(/from\s+["']@hexclave\/([a-z0-9-]+)(?:\/config)?["']/i);
if (hexclave) return `@hexclave/${hexclave[1]}`;
const stackframe = currentFileContent.match(/from\s+["']@stackframe\/([a-z0-9-]+)["']/i);
const stackframe = currentFileContent.match(/from\s+["']@stackframe\/([a-z0-9-]+)(?:\/config)?["']/i);
return stackframe ? `@stackframe/${stackframe[1]}` : undefined;
}

View File

@ -1,14 +1,25 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
import { join } from "path";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("server-only", () => ({}));
// Root temp config files next to this test file (inside apps/dashboard) rather
// than at process.cwd() (the repo root under vitest's workspace runner). This
// lets jiti resolve workspace packages like `@hexclave/next/config` the same
// way a real user project would — walking up to apps/dashboard/node_modules.
const TEST_FILE_DIR = dirname(fileURLToPath(import.meta.url));
let tempDir: string | undefined;
function createTempDir(): string {
tempDir ??= mkdtempSync(join(TEST_FILE_DIR, ".stack-rde-config-test-"));
return tempDir;
}
function writeTempConfig(content: string): string {
tempDir ??= mkdtempSync(join(process.cwd(), ".stack-rde-config-test-"));
const configPath = join(tempDir, "stack.config.ts");
const configPath = join(createTempDir(), "stack.config.ts");
writeFileSync(configPath, content, "utf-8");
return configPath;
}
@ -24,7 +35,7 @@ afterEach(() => {
describe("remote development environment config file", () => {
it("loads config exports wrapped in defineStackConfig", async () => {
const configPath = writeTempConfig(`
import { defineStackConfig } from "@hexclave/shared/config";
import { defineStackConfig } from "@hexclave/next/config";
export const config = defineStackConfig({
auth: {
@ -49,7 +60,7 @@ describe("remote development environment config file", () => {
it("loads config exports wrapped in defineHexclaveConfig", async () => {
const configPath = writeTempConfig(`
import { defineHexclaveConfig } from "@hexclave/shared/config";
import { defineHexclaveConfig } from "@hexclave/next/config";
export const config = defineHexclaveConfig({
auth: {
@ -155,6 +166,24 @@ describe("remote development environment config file", () => {
`);
});
it("throws a helpful error when the config file imports a module that fails to load", async () => {
// Simulate a heavy framework package (e.g. @stackframe/stack) that throws on import
const dir = createTempDir();
const heavyPackagePath = join(dir, "heavy-package.ts");
writeFileSync(heavyPackagePath, `throw new Error("Cannot load this in a Node.js context");`, "utf-8");
const configPath = join(dir, "stack.config.ts");
writeFileSync(configPath, `
import "${heavyPackagePath}";
export const config = {};
`, "utf-8");
const { readConfigFile } = await import("./config-file");
await expect(readConfigFile(configPath)).rejects.toThrow(
`Failed to load config file ${configPath}. If your config imports a value (e.g. defineHexclaveConfig) from a framework package such as "@hexclave/next", import it from that package's lightweight "/config" entrypoint instead`
);
});
it("rejects modules without a valid config export", async () => {
const configPath = writeTempConfig(`
export const config = () => ({ auth: { allowSignUp: true } });
@ -173,6 +202,10 @@ describe("remote development environment config file", () => {
},
};
`);
// Pin the SDK package the rendered import line points at, so the snapshot
// doesn't depend on which @hexclave/* package the surrounding workspace
// (apps/dashboard) happens to depend on.
writeFileSync(join(createTempDir(), "package.json"), JSON.stringify({ dependencies: { "@hexclave/js": "*" } }), "utf-8");
const { readConfigFile, writeConfigObject } = await import("./config-file");
const current = await readConfigFile(configPath);
@ -182,7 +215,7 @@ describe("remote development environment config file", () => {
});
expect(readFileSync(configPath, "utf-8")).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"auth": {

View File

@ -3,6 +3,7 @@ import "server-only";
import { showOnboardingHexclaveConfigValue } from "@hexclave/shared/dist/config-authoring";
import { Config, isValidConfig } from "@hexclave/shared/dist/config/format";
import { detectImportPackageFromDir, renderConfigFileContent } from "@hexclave/shared/dist/config-rendering";
import { captureError } from "@hexclave/shared/dist/utils/errors";
import { createHash } from "crypto";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
import { createJiti } from "jiti";
@ -62,7 +63,18 @@ export async function readConfigFile(configFilePath: string): Promise<{ config:
};
}
const configModule = await jiti.import<unknown>(configFilePath);
let configModule: unknown;
try {
configModule = await jiti.import<unknown>(configFilePath);
} catch (error) {
// Capture the raw jiti/framework error for diagnostics, but don't attach it as `cause` on the thrown error:
// the dashboard's error formatter (errorToNiceString -> nicify) renders `Error.cause` recursively, which would
// leak the underlying framework stack/internals back into the user-facing message we're deliberately replacing.
captureError("remote-development-environment/readConfigFile", error);
throw new Error(
`Failed to load config file ${configFilePath}. If your config imports a value (e.g. defineHexclaveConfig) from a framework package such as "@hexclave/next", import it from that package's lightweight "/config" entrypoint instead, which doesn't load the framework runtime:\n\n import { defineHexclaveConfig } from "@hexclave/next/config";\n`,
);
}
if (!isConfigModule(configModule)) {
throw new Error(`Invalid config in ${configFilePath}. The file must export a plain \`config\` object or "show-onboarding".`);
}

View File

@ -115,7 +115,7 @@ describe("local emulator config restrictions", () => {
const fileContent = await fs.readFile(filePath, "utf-8");
expect(fileContent).toMatchInlineSnapshot(`
deindent\`
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"teams": {

View File

@ -461,7 +461,7 @@ describe("Stack CLI", () => {
expect(exitCode).toBe(0);
expect(stdout).toContain("Config written to");
const content = fs.readFileSync(configTsPath, "utf-8");
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js/config";');
expect(content).toContain("export const config: HexclaveConfig");
});
@ -556,7 +556,7 @@ describe("Stack CLI", () => {
expect(stdout).toContain("Config file written to");
const content = fs.readFileSync(path.join(initDir, "stack.config.ts"), "utf-8");
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js/config";');
expect(content).toContain("export const config: HexclaveConfig");
expect(JSON.parse(extractConfigObjectString(content))).toMatchObject({
apps: {

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@ sidebarTitle: "hexclave.config.ts"
The file exports a static `config` object:
```ts title="hexclave.config.ts"
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
auth: {
@ -28,6 +28,22 @@ export const config: HexclaveConfig = {
};
```
<Note>
Always import config helpers from the package's lightweight `/config` entrypoint (e.g. `@hexclave/js/config`, `@hexclave/next/config`) rather than the package root. The `/config` entrypoint contains no framework runtime code, so tooling such as the local dashboard can load your config file in a plain Node context. Importing `defineHexclaveConfig` (or the `HexclaveConfig` type) from the package root instead would pull in the entire SDK and fail to load.
</Note>
To get type-checking and editor autocomplete for your config object, wrap it with `defineHexclaveConfig`:
```ts title="hexclave.config.ts"
import { defineHexclaveConfig } from "@hexclave/js/config";
export const config = defineHexclaveConfig({
auth: {
allowSignUp: true,
},
});
```
If you are running Hexclave with a [local dashboard](/guides/going-further/local-vs-cloud-dashboard), you already have a `hexclave.config.ts` file, and any changes you make on the dashboard will automatically be synced to the config file.
If you are running Hexclave on a [cloud project](/guides/going-further/local-vs-cloud-dashboard) instead, you may need to use the [CLI's `pull` and `push`](/guides/going-further/cli#config-commands) commands to sync your config file with the cloud. In production, you would usually do this in your GitHub Actions or CI/CD pipeline.

View File

@ -25,7 +25,7 @@ Use a development environment when you want to:
The usual setup looks like this:
```ts title="hexclave.config.ts"
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = "show-onboarding";
```

View File

@ -231,12 +231,14 @@ The frameworks and languages with explicit SDK support are:
First, create a `hexclave.config.ts` configuration file in the root directory of the workspace (or anywhere else):
```ts hexclave.config.ts
import type { HexclaveConfig } from "<the-sdk-from-above>";
import type { HexclaveConfig } from "<the-sdk-from-above>/config";
// default: show-onboarding, which shows the onboarding flow for this project when Hexclave starts
export const config: HexclaveConfig = "show-onboarding";
```
The `/config` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with `defineHexclaveConfig` imported from the same `<the-sdk-from-above>/config` path (never from `<the-sdk-from-above>` directly, which would pull in the whole SDK and fail to load).
To run your application with Hexclave, you can then start the dev environment and set environment variables expected by your application. Hexclave's CLI has a `dev` command does both of these, so let's install it as a dev dependency and wrap your existing `dev` script in your package.json:
```sh
@ -782,11 +784,13 @@ This setup is for Python backends that do not use the JavaScript SDK. The backen
If this project already has a `hexclave.config.ts` file for another frontend or backend, reuse that same file so the whole project shares one Hexclave config. Otherwise, create a new `hexclave.config.ts` file in your workspace:
```ts hexclave.config.ts
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = "show-onboarding";
```
The `/config` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with `defineHexclaveConfig` imported from the same `@hexclave/js/config` path (never from `@hexclave/js` directly, which would pull in the whole SDK and fail to load).
Run your backend through the Hexclave CLI so it starts the local dashboard and injects the Hexclave environment variables:
```json package.json
@ -931,11 +935,13 @@ Use this option when your backend is not JavaScript/TypeScript or Python, or whe
If this project already has a `hexclave.config.ts` file for another frontend or backend, reuse that same file so the whole project shares one Hexclave config. Otherwise, create a new `hexclave.config.ts` file in your workspace:
```ts hexclave.config.ts
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = "show-onboarding";
```
The `/config` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with `defineHexclaveConfig` imported from the same `@hexclave/js/config` path (never from `@hexclave/js` directly, which would pull in the whole SDK and fail to load).
Run your backend through the Hexclave CLI so it starts the local dashboard and injects the Hexclave environment variables:
```json package.json

File diff suppressed because one or more lines are too long

View File

@ -278,7 +278,10 @@ export function registerConfigCommand(program: Command) {
const config = parseConfigOverride(configModule.config);
if (config == null) {
const examplePkg = detectImportPackageFromDir(path.dirname(filePath)) ?? "@hexclave/js";
throw new CliError(`Config file must export a plain \`config\` object or "show-onboarding". Example: import type { StackConfig } from "${examplePkg}"; export const config: StackConfig = { ... };`);
// The lightweight `/config` entrypoint only exists on Hexclave-branded packages;
// legacy `@stackframe/*` releases predate it, so import from their root.
const exampleImport = examplePkg.startsWith("@hexclave/") ? `${examplePkg}/config` : examplePkg;
throw new CliError(`Config file must export a plain \`config\` object or "show-onboarding". Example: import type { HexclaveConfig } from "${exampleImport}"; export const config: HexclaveConfig = { ... };`);
}
const source = buildConfigPushSource(opts.configFile, {

View File

@ -16,6 +16,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./convex.config": {
"types": "./dist/integrations/convex/component/convex.config.d.ts",
"import": {

View File

@ -16,6 +16,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./convex.config": {
"types": "./dist/integrations/convex/component/convex.config.d.ts",
"import": {

View File

@ -16,6 +16,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./convex.config": {
"types": "./dist/integrations/convex/component/convex.config.d.ts",
"import": {

View File

@ -442,11 +442,13 @@ function getRestBackendSetupPrompt(kind: "python" | "rest-api") {
If this project already has a \`hexclave.config.ts\` file for another frontend or backend, reuse that same file so the whole project shares one Hexclave config. Otherwise, create a new \`hexclave.config.ts\` file in your workspace:
\`\`\`ts hexclave.config.ts
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = "show-onboarding";
\`\`\`
The \`/config\` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with \`defineHexclaveConfig\` imported from the same \`@hexclave/js/config\` path (never from \`@hexclave/js\` directly, which would pull in the whole SDK and fail to load).
Run your backend through the Hexclave CLI so it starts the local dashboard and injects the Hexclave environment variables:
\`\`\`json package.json
@ -723,12 +725,14 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
First, create a \`hexclave.config.ts\` configuration file in the root directory of the workspace (or anywhere else):
\`\`\`ts hexclave.config.ts
import type { HexclaveConfig } from "${packageName}";
import type { HexclaveConfig } from "${packageName}/config";
// default: show-onboarding, which shows the onboarding flow for this project when Hexclave starts
export const config: HexclaveConfig = "show-onboarding";
\`\`\`
The \`/config\` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with \`defineHexclaveConfig\` imported from the same \`${packageName}/config\` path (never from \`${packageName}\` directly, which would pull in the whole SDK and fail to load).
To run your application with Hexclave, you can then start the dev environment and set environment variables expected by your application. Hexclave's CLI has a \`dev\` command does both of these, so let's install it as a dev dependency and wrap your existing \`dev\` script in your package.json:
\`\`\`sh

View File

@ -13,6 +13,7 @@ export { parseHexclaveConfigFileContent, renderConfigFileContent };
const CONFIG_IMPORT_PACKAGES = [
"@hexclave/next",
"@hexclave/react",
"@hexclave/tanstack-start",
"@hexclave/js",
"@hexclave/template",
"@stackframe/stack",
@ -120,18 +121,26 @@ import.meta.vitest?.test("renderConfigFileContent rejects invalid config exports
import.meta.vitest?.test("renderConfigFileContent uses custom import package", ({ expect }) => {
const content = renderConfigFileContent({}, "@hexclave/next");
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/next";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/next/config";');
});
import.meta.vitest?.test("renderConfigFileContent defaults to @hexclave/js", ({ expect }) => {
const content = renderConfigFileContent({});
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js/config";');
});
import.meta.vitest?.test("renderConfigFileContent keeps legacy @stackframe packages on their root entrypoint", ({ expect }) => {
// The lightweight `/config` subpath only exists on Hexclave-branded packages;
// already-published @stackframe/* releases predate it.
const content = renderConfigFileContent({}, "@stackframe/next");
expect(content).toContain('import type { HexclaveConfig } from "@stackframe/next";');
});
import.meta.vitest?.test("detectConfigImportPackage picks first matching package by priority", ({ expect }) => {
expect(detectConfigImportPackage(["@hexclave/next", "@hexclave/js"])).toBe("@hexclave/next");
expect(detectConfigImportPackage(["@hexclave/react", "@hexclave/js"])).toBe("@hexclave/react");
expect(detectConfigImportPackage(["@hexclave/js"])).toBe("@hexclave/js");
expect(detectConfigImportPackage(["@hexclave/tanstack-start"])).toBe("@hexclave/tanstack-start");
// Hexclave names take priority over legacy stackframe names when both appear.
expect(detectConfigImportPackage(["@stackframe/stack", "@hexclave/next"])).toBe("@hexclave/next");
// Legacy fallback still works for projects pinned to the last @stackframe/* release.

View File

@ -28,7 +28,13 @@ export function renderConfigFileContent(config: unknown, importPackage?: string)
throw new Error(`Config has conflicting keys that would be dropped during normalization: ${droppedKeys.map(k => JSON.stringify(k)).join(", ")}`);
}
const pkg = importPackage ?? DEFAULT_CONFIG_IMPORT_PACKAGE;
const importLine = `import type { HexclaveConfig } from "${pkg}";`;
// Import the `HexclaveConfig` type from the package's lightweight `/config`
// entrypoint, which is free of framework runtime code and therefore safe for
// tooling (e.g. the local dashboard) to load in a plain Node context. Only the
// Hexclave-branded packages expose this subpath; legacy `@stackframe/*`
// releases predate it, so fall back to their package root.
const importSpecifier = pkg.startsWith("@hexclave/") ? `${pkg}/config` : pkg;
const importLine = `import type { HexclaveConfig } from "${importSpecifier}";`;
return `${importLine}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
}

View File

@ -16,6 +16,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./tanstack-start-server-context": {
"types": "./dist/tanstack-start-server-context.combined.d.ts",
"import": {

View File

@ -28,6 +28,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"//": "IF_PLATFORM tanstack-start",
"./tanstack-start-server-context": {
"types": "./dist/tanstack-start-server-context.combined.d.ts",

View File

@ -17,6 +17,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./tanstack-start-server-context": {
"types": "./dist/tanstack-start-server-context.combined.d.ts",
"import": {

View File

@ -0,0 +1,13 @@
// Lightweight, side-effect-free entrypoint for authoring `hexclave.config.ts`
// files. Importing from here (e.g. `@hexclave/next/config`) gives you the
// `defineHexclaveConfig` helper and config types WITHOUT pulling in the
// framework runtime (React, server-only, Next.js internals). That matters
// because tooling such as the local dashboard evaluates your config file in a
// plain Node context — importing `defineHexclaveConfig` from the package root
// would drag in the whole SDK and fail to load.
//
// Hexclave aliases and legacy Stack* names — @deprecated JSDoc lives on the
// original declarations in @hexclave/shared/config so it survives dts bundling
// (per-specifier JSDoc on re-exports does not).
export type { HexclaveConfig, StackConfig } from "@hexclave/shared/config";
export { defineHexclaveConfig, defineStackConfig, showOnboardingHexclaveConfigValue } from "@hexclave/shared/config";