mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-21 21:09:49 +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 -->
152 lines
5.8 KiB
TypeScript
152 lines
5.8 KiB
TypeScript
import { existsSync, readFileSync } from "fs";
|
|
import path from "path";
|
|
import { parseHexclaveConfigFileContent, renderConfigFileContent } from "./hexclave-config-file";
|
|
export { parseHexclaveConfigFileContent, renderConfigFileContent };
|
|
|
|
/**
|
|
* Packages that export the `HexclaveConfig` type, in priority order.
|
|
* The first match found in a project's dependencies wins. Hexclave-branded
|
|
* packages come first (canonical); the legacy `@stackframe/*` names remain
|
|
* so projects pinned to the last legacy release still render a config file
|
|
* that compiles against their installed SDK.
|
|
*/
|
|
const CONFIG_IMPORT_PACKAGES = [
|
|
"@hexclave/next",
|
|
"@hexclave/react",
|
|
"@hexclave/tanstack-start",
|
|
"@hexclave/js",
|
|
"@hexclave/template",
|
|
"@stackframe/stack",
|
|
"@stackframe/react",
|
|
"@stackframe/js",
|
|
"@stackframe/template",
|
|
] as const;
|
|
|
|
/**
|
|
* Given a list of dependency names (from package.json), returns the SDK
|
|
* package that should be used for the `HexclaveConfig` import, or `undefined`
|
|
* if none of the known packages are installed.
|
|
*/
|
|
export function detectConfigImportPackage(dependencies: string[]): string | undefined {
|
|
for (const pkg of CONFIG_IMPORT_PACKAGES) {
|
|
if (dependencies.includes(pkg)) {
|
|
return pkg;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Walks up from `dir` to find the nearest `package.json` and returns the
|
|
* best SDK package to use for the `HexclaveConfig` type import.
|
|
*/
|
|
export function detectImportPackageFromDir(dir: string): string | undefined {
|
|
let current = dir;
|
|
while (true) {
|
|
const pkgPath = path.join(current, "package.json");
|
|
if (existsSync(pkgPath)) {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
const deps = [
|
|
...Object.keys(pkg.dependencies ?? {}),
|
|
...Object.keys(pkg.devDependencies ?? {}),
|
|
];
|
|
return detectConfigImportPackage(deps);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
const parent = path.dirname(current);
|
|
if (parent === current) break;
|
|
current = parent;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
import.meta.vitest?.test("renderConfigFileContent normalizes config exports", ({ expect }) => {
|
|
expect(renderConfigFileContent({
|
|
"payments.items.todos.displayName": "Todo Slots",
|
|
"payments.items.todos.customerType": "user",
|
|
})).toContain(`export const config: HexclaveConfig = {
|
|
"payments": {
|
|
"items": {
|
|
"todos": {
|
|
"displayName": "Todo Slots",
|
|
"customerType": "user"
|
|
}
|
|
}
|
|
}
|
|
};`);
|
|
});
|
|
|
|
import.meta.vitest?.test("parseHexclaveConfigFileContent parses static config exports", ({ expect }) => {
|
|
expect(parseHexclaveConfigFileContent(`
|
|
import type { StackConfig } from "@hexclave/js";
|
|
export const config: StackConfig = {
|
|
auth: { allowSignUp: true },
|
|
payments: { testMode: false },
|
|
};
|
|
`, "stack.config.ts")).toMatchInlineSnapshot(`
|
|
{
|
|
"auth": {
|
|
"allowSignUp": true,
|
|
},
|
|
"payments": {
|
|
"testMode": false,
|
|
},
|
|
}
|
|
`);
|
|
});
|
|
|
|
import.meta.vitest?.test("parseHexclaveConfigFileContent parses show-onboarding", ({ expect }) => {
|
|
expect(parseHexclaveConfigFileContent('export const config = "show-onboarding";', "stack.config.ts")).toBe("show-onboarding");
|
|
});
|
|
|
|
import.meta.vitest?.test("parseHexclaveConfigFileContent rejects dynamic config exports", ({ expect }) => {
|
|
expect(() => parseHexclaveConfigFileContent("export const config = makeConfig();", "stack.config.ts")).toThrow(/Unsupported config expression/);
|
|
});
|
|
|
|
import.meta.vitest?.test("renderConfigFileContent rejects conflicting dotted keys", ({ expect }) => {
|
|
expect(() => renderConfigFileContent({
|
|
"a.b": 1,
|
|
"a.b.c": 2,
|
|
})).toThrowError(/conflicting keys.*"a\.b\.c"/);
|
|
});
|
|
|
|
import.meta.vitest?.test("renderConfigFileContent rejects invalid config exports", ({ expect }) => {
|
|
expect(() => renderConfigFileContent(null)).toThrowErrorMatchingInlineSnapshot(
|
|
`[Error: Invalid config: expected a plain object.]`,
|
|
);
|
|
});
|
|
|
|
import.meta.vitest?.test("renderConfigFileContent uses custom import package", ({ expect }) => {
|
|
const content = renderConfigFileContent({}, "@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/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.
|
|
expect(detectConfigImportPackage(["@stackframe/stack"])).toBe("@stackframe/stack");
|
|
expect(detectConfigImportPackage(["@stackframe/template"])).toBe("@stackframe/template");
|
|
expect(detectConfigImportPackage(["lodash", "express"])).toBeUndefined();
|
|
expect(detectConfigImportPackage([])).toBeUndefined();
|
|
});
|