mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-19 21:00:40 +08:00
## 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 -->
116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
/**
|
|
* Pure logic for taking a config update produced by the dashboard, merging it
|
|
* into the user's GitHub-stored `stack.config.ts` file, and committing the
|
|
* result back to GitHub via the Contents API.
|
|
*
|
|
* `buildUpdatedConfigFileContent` is the pure heart of this module — it's
|
|
* directly unit-testable, takes the current file content and a config update,
|
|
* and returns the new file content. The orchestrator `pushConfigUpdateToGitHub`
|
|
* wires it up to GitHub's REST API.
|
|
*/
|
|
|
|
import type { PushedConfigSource } from "@hexclave/next";
|
|
import type { EnvironmentConfigOverrideOverride } from "@hexclave/shared/dist/config/schema";
|
|
import { isValidConfig, override } from "@hexclave/shared/dist/config/format";
|
|
import { parseHexclaveConfigFileContent, renderConfigFileContent, showOnboardingHexclaveConfigValue } from "@hexclave/shared/dist/hexclave-config-file";
|
|
|
|
import {
|
|
commitFile,
|
|
getFileContent,
|
|
type GithubFetch,
|
|
} from "./github-api";
|
|
|
|
/**
|
|
* Detects the `@hexclave/*` or legacy `@stackframe/*` import package used by
|
|
* the existing config file so the re-rendered file keeps the same import
|
|
* line. Falls back to `@hexclave/js` when the file is empty or the import
|
|
* cannot be detected.
|
|
*/
|
|
function detectImportPackage(currentFileContent: string): string | undefined {
|
|
// Match `from "@hexclave/<name>"` or `from "@stackframe/<name>"` — single
|
|
// 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-]+)(?:\/config)?["']/i);
|
|
return stackframe ? `@stackframe/${stackframe[1]}` : undefined;
|
|
}
|
|
|
|
/**
|
|
* Pure: given the existing contents of a `stack.config.ts` file and a config
|
|
* update (the same dot-notation override shape that flows through
|
|
* `updatePushedConfig`), returns the new file contents.
|
|
*
|
|
* The existing import line is preserved when the source file imports
|
|
* `StackConfig` from a known `@hexclave/*` or legacy `@stackframe/*` package;
|
|
* otherwise the renderer uses its own default.
|
|
*/
|
|
export function buildUpdatedConfigFileContent(
|
|
currentFileContent: string,
|
|
configUpdate: EnvironmentConfigOverrideOverride,
|
|
): string {
|
|
const parsed = parseHexclaveConfigFileContent(currentFileContent, "stack.config.ts");
|
|
if (parsed === showOnboardingHexclaveConfigValue) {
|
|
throw new Error(
|
|
"The config file currently exports the onboarding placeholder. Finish setting up Hexclave in your repo before pushing dashboard changes."
|
|
);
|
|
}
|
|
if (!isValidConfig(parsed)) {
|
|
throw new Error("Existing GitHub config file does not parse as a valid Hexclave config object.");
|
|
}
|
|
const merged = override(parsed, configUpdate);
|
|
const importPackage = detectImportPackage(currentFileContent);
|
|
return renderConfigFileContent(merged, importPackage);
|
|
}
|
|
|
|
export type PushConfigUpdateOptions = {
|
|
source: Extract<PushedConfigSource, { type: "pushed-from-github" }>,
|
|
configUpdate: EnvironmentConfigOverrideOverride,
|
|
commitMessage: string,
|
|
githubFetch: GithubFetch,
|
|
};
|
|
|
|
/**
|
|
* Pushes a config update to GitHub by editing the user's `stack.config.ts`
|
|
* file in place via the Contents API. The accompanying GitHub Actions workflow
|
|
* (added in onboarding) will pick up the commit and re-push the canonical
|
|
* config back to Hexclave.
|
|
*
|
|
* Commits the updated config file when needed; returns once GitHub accepts the
|
|
* write.
|
|
*/
|
|
export async function pushConfigUpdateToGitHub(options: PushConfigUpdateOptions): Promise<void> {
|
|
const { source, configUpdate, commitMessage, githubFetch } = options;
|
|
const { owner, repo, branch, configFilePath } = source;
|
|
|
|
const existing = await getFileContent(githubFetch, { owner, repo, branch, path: configFilePath });
|
|
if (existing == null) {
|
|
throw new Error(
|
|
`Could not find ${configFilePath} on ${owner}/${repo}@${branch}. Check that the config file still exists in the linked branch.`
|
|
);
|
|
}
|
|
|
|
const newContent = buildUpdatedConfigFileContent(existing.text, configUpdate);
|
|
if (newContent === existing.text) {
|
|
// Nothing changed in the rendered file — no need to commit. The dashboard
|
|
// will still update the cloud-side override for immediate feedback.
|
|
return;
|
|
}
|
|
|
|
const trimmedMessage = commitMessage.trim().length > 0
|
|
? commitMessage.trim()
|
|
: "chore(stack-auth): update config from dashboard";
|
|
|
|
await commitFile(githubFetch, {
|
|
owner,
|
|
repo,
|
|
branch,
|
|
path: configFilePath,
|
|
content: newContent,
|
|
message: trimmedMessage,
|
|
sha: existing.sha,
|
|
});
|
|
}
|