mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-19 21:00:40 +08:00
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:
parent
88ef2ce85f
commit
7f99f15b42
@ -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`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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".`);
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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
@ -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.
|
||||
|
||||
@ -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";
|
||||
```
|
||||
|
||||
@ -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
@ -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, {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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`;
|
||||
}
|
||||
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
13
packages/template/src/config.ts
Normal file
13
packages/template/src/config.ts
Normal 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";
|
||||
Loading…
Reference in New Issue
Block a user