mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
Install deps in config-sync workflow + use root SDK imports; drop CLI /config alias
Some checks are pending
DB migration compat / Check if migrations changed (push) Waiting to run
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Blocked by required conditions
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Blocked by required conditions
DB migration compat / No migration changes (skipped) (push) Blocked by required conditions
Some checks are pending
DB migration compat / Check if migrations changed (push) Waiting to run
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Blocked by required conditions
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Blocked by required conditions
DB migration compat / No migration changes (skipped) (push) Blocked by required conditions
Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
parent
4df9df5870
commit
36d982d1e5
@ -36,6 +36,19 @@ describe("buildWorkflowYaml", () => {
|
||||
expect(workflowYaml).toContain("HEXCLAVE_SOURCE_REPO: ${{ github.repository }}");
|
||||
expect(workflowYaml).not.toMatch(/HEXCLAVE_SOURCE_REPO:\s+"[^$]/);
|
||||
});
|
||||
|
||||
it("installs the repo's dependencies (with lockfile detection) before pushing config", () => {
|
||||
const workflowYaml = buildWorkflowYaml("main", "hexclave.config.ts");
|
||||
expect(workflowYaml).toContain("- name: Install dependencies");
|
||||
for (const marker of ["pnpm-lock.yaml", "yarn.lock", "package-lock.json", "npm ci"]) {
|
||||
expect(workflowYaml).toContain(marker);
|
||||
}
|
||||
// The install must run before the push step, otherwise the SDK import would
|
||||
// still be unresolvable when the CLI evaluates the config.
|
||||
expect(workflowYaml.indexOf("- name: Install dependencies")).toBeLessThan(
|
||||
workflowYaml.indexOf("- name: Push Hexclave config"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeConfigPath", () => {
|
||||
|
||||
@ -52,6 +52,27 @@ jobs:
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
# Install the repo's dependencies so the config file's SDK import (e.g.
|
||||
# \`@hexclave/js\`) — and any other package it imports — resolves when the
|
||||
# CLI evaluates it. The package manager is detected from the lockfile.
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
if [ -f pnpm-lock.yaml ]; then
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
elif [ -f yarn.lock ]; then
|
||||
corepack enable
|
||||
yarn install --immutable
|
||||
elif [ -f bun.lockb ] || [ -f bun.lock ]; then
|
||||
npm install -g bun
|
||||
bun install --frozen-lockfile
|
||||
elif [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then
|
||||
npm ci
|
||||
elif [ -f package.json ]; then
|
||||
npm install
|
||||
else
|
||||
echo "No package.json found at the repository root; skipping dependency install."
|
||||
fi
|
||||
- name: Push Hexclave config
|
||||
env:
|
||||
HEXCLAVE_PROJECT_ID: \${{ secrets.${GITHUB_PROJECT_ID_SECRET_NAME} }}
|
||||
|
||||
@ -399,7 +399,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/config";');
|
||||
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
|
||||
expect(content).toContain("export const config: HexclaveConfig");
|
||||
});
|
||||
|
||||
@ -514,7 +514,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/config";');
|
||||
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
|
||||
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/config";
|
||||
import type { HexclaveConfig } from "@hexclave/js";
|
||||
|
||||
export const config: HexclaveConfig = {
|
||||
auth: {
|
||||
@ -28,14 +28,10 @@ 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";
|
||||
import { defineHexclaveConfig } from "@hexclave/js";
|
||||
|
||||
export const config = defineHexclaveConfig({
|
||||
auth: {
|
||||
|
||||
@ -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/config";
|
||||
import type { HexclaveConfig } from "@hexclave/js";
|
||||
|
||||
export const config: HexclaveConfig = "show-onboarding";
|
||||
```
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -2,7 +2,7 @@ import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { assertConfigPullTarget, buildConfigImportAlias, buildConfigPushSource, resolveConfigFilePathForPull } from "./config-file.js";
|
||||
import { assertConfigPullTarget, buildConfigPushSource, resolveConfigFilePathForPull } from "./config-file.js";
|
||||
|
||||
describe("resolveConfigFilePathForPull", () => {
|
||||
let tmpDir: string;
|
||||
@ -80,62 +80,6 @@ describe("assertConfigPullTarget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildConfigImportAlias", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "stack-cli-config-alias-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("aliases unresolvable SDK config entrypoints to the CLI's own @hexclave/js/config", () => {
|
||||
const alias = buildConfigImportAlias(tmpDir);
|
||||
// The legacy `@stackframe/*` packages are not part of this monorepo, so
|
||||
// they're never resolvable from the config file's directory and must be
|
||||
// aliased. (`@hexclave/*` packages may or may not resolve depending on the
|
||||
// environment's module resolution, so we don't assert on those here.)
|
||||
expect(alias["@stackframe/stack"]).toBeDefined();
|
||||
// Every aliased specifier points to the same single real file — the CLI's
|
||||
// bundled `@hexclave/js/config`.
|
||||
const targets = new Set(Object.values(alias));
|
||||
expect(targets.size).toBe(1);
|
||||
const target = [...targets][0];
|
||||
expect(fs.existsSync(target)).toBe(true);
|
||||
// Only known SDK config specifiers are aliased.
|
||||
for (const specifier of Object.keys(alias)) {
|
||||
expect(specifier === "@stackframe/stack" || specifier === "@stackframe/react" || specifier === "@stackframe/js" || specifier === "@stackframe/template" || specifier.endsWith("/config")).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("lets jiti evaluate a config that imports defineHexclaveConfig without the SDK installed", async () => {
|
||||
// Reproduces the CI sync workflow: the repo is checked out but its
|
||||
// dependencies aren't installed, so `@hexclave/js/config` is unresolvable
|
||||
// from the config file's directory.
|
||||
const configPath = path.join(tmpDir, "hexclave.config.ts");
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
[
|
||||
`import { defineHexclaveConfig } from "@hexclave/js/config";`,
|
||||
``,
|
||||
`export const config = defineHexclaveConfig({`,
|
||||
` auth: { allowSignUp: true },`,
|
||||
`});`,
|
||||
``,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const { createJiti } = await import("jiti");
|
||||
const jiti = createJiti(import.meta.url, {
|
||||
alias: buildConfigImportAlias(path.dirname(configPath)),
|
||||
});
|
||||
const mod: { config?: unknown } = await jiti.import(configPath);
|
||||
expect(mod.config).toEqual({ auth: { allowSignUp: true } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildConfigPushSource", () => {
|
||||
const ORIGINAL_ENV = { ...process.env };
|
||||
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { replaceConfigObject } from "@hexclave/shared-backend";
|
||||
import { detectImportPackageFromDir } from "@hexclave/shared/dist/config-eval";
|
||||
import { CONFIG_IMPORT_PACKAGES, configImportSpecifierForPackage } from "@hexclave/shared/dist/config-rendering";
|
||||
import { isValidConfig } from "@hexclave/shared/dist/config/format";
|
||||
import type { EnvironmentConfigOverrideOverride } from "@hexclave/shared/dist/config/schema";
|
||||
import { throwErr } from "@hexclave/shared/dist/utils/errors";
|
||||
import { Command } from "commander";
|
||||
import * as fs from "fs";
|
||||
import { createRequire } from "module";
|
||||
import * as path from "path";
|
||||
import { getAdminProject } from "../lib/app.js";
|
||||
import { isProjectAuthWithRefreshToken, isProjectAuthWithSecretServerKey, resolveAuth, resolveProjectId, type ProjectAuthWithSecretServerKey } from "../lib/auth.js";
|
||||
@ -235,35 +233,6 @@ export function assertConfigPullTarget(filePath: string, opts: { overwrite?: boo
|
||||
}
|
||||
}
|
||||
|
||||
// Typed config files import `defineHexclaveConfig` (a runtime helper) and the
|
||||
// `HexclaveConfig` type from an SDK's lightweight `/config` entrypoint, e.g.
|
||||
// `@hexclave/js/config`. `config push` runs from the generated GitHub sync
|
||||
// workflow, which checks out the repo and runs the CLI via `npx` WITHOUT
|
||||
// installing the repo's dependencies — so jiti cannot resolve that import and
|
||||
// evaluating the config fails with MODULE_NOT_FOUND. The CLI bundles
|
||||
// `@hexclave/js`, and every SDK `/config` entrypoint just re-exports the same
|
||||
// identity helper + types from `@hexclave/shared/config`, so we resolve those
|
||||
// imports to the CLI's own copy as a fallback. We only alias specifiers that
|
||||
// don't already resolve from the config file's directory, so a project that
|
||||
// does have its SDK installed keeps using its own copy.
|
||||
export function buildConfigImportAlias(configFileDir: string): Record<string, string> {
|
||||
const fallbackTarget = createRequire(import.meta.url).resolve("@hexclave/js/config");
|
||||
// `createRequire` needs a file path to anchor resolution; the file need not
|
||||
// exist, it only seeds the module search paths (node_modules lookup walks up
|
||||
// from this directory).
|
||||
const fromConfigDir = createRequire(path.join(configFileDir, "__hexclave_config_resolver__.cjs"));
|
||||
const alias: Record<string, string> = {};
|
||||
for (const pkg of CONFIG_IMPORT_PACKAGES) {
|
||||
const specifier = configImportSpecifierForPackage(pkg);
|
||||
try {
|
||||
fromConfigDir.resolve(specifier);
|
||||
} catch {
|
||||
alias[specifier] = fallbackTarget;
|
||||
}
|
||||
}
|
||||
return alias;
|
||||
}
|
||||
|
||||
export function registerConfigCommand(program: Command) {
|
||||
const config = program
|
||||
.command("config")
|
||||
@ -318,18 +287,16 @@ export function registerConfigCommand(program: Command) {
|
||||
throw new CliError("Config file must have a .js or .ts extension.");
|
||||
}
|
||||
|
||||
// The generated GitHub sync workflow installs the repo's dependencies
|
||||
// before running the CLI, so jiti resolves the config's SDK import (e.g.
|
||||
// `@hexclave/js`) from the project's own node_modules.
|
||||
const { createJiti } = await import("jiti");
|
||||
const jiti = createJiti(import.meta.url, {
|
||||
alias: buildConfigImportAlias(path.dirname(filePath)),
|
||||
});
|
||||
const jiti = createJiti(import.meta.url);
|
||||
const configModule: { config?: unknown } = await jiti.import(filePath);
|
||||
|
||||
const config = parseConfigOverride(configModule.config);
|
||||
if (config == null) {
|
||||
const examplePkg = detectImportPackageFromDir(path.dirname(filePath)) ?? "@hexclave/js";
|
||||
// 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;
|
||||
const exampleImport = detectImportPackageFromDir(path.dirname(filePath)) ?? "@hexclave/js";
|
||||
throw new CliError(`Config file must export a plain \`config\` object or "show-onboarding". Example: import type { HexclaveConfig } from "${exampleImport}"; export const config: HexclaveConfig = { ... };`);
|
||||
}
|
||||
|
||||
|
||||
@ -411,12 +411,12 @@ 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/config";
|
||||
import type { HexclaveConfig } from "@hexclave/js";
|
||||
|
||||
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).
|
||||
If you later switch to a config object and want type-checking, wrap it with \`defineHexclaveConfig\`, imported from the same \`@hexclave/js\` package.
|
||||
|
||||
Run your backend through the Hexclave CLI so it starts the local dashboard and injects the Hexclave environment variables:
|
||||
|
||||
@ -975,13 +975,13 @@ 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}/config";
|
||||
import type { HexclaveConfig } from "${packageName}";
|
||||
|
||||
// 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).
|
||||
If you later switch to a config object and want type-checking, wrap it with \`defineHexclaveConfig\`, imported from the same \`${packageName}\` package.
|
||||
|
||||
${isAiPrompt ? deindent`
|
||||
If you already know which apps you want to enable and how to configure them, you can also set the \`config\` object to the desired configuration directly. Refer to the per-app setup instructions for more information. However, in most cases, you would probably want to let the user onboard manually through the show-onboarding flow.
|
||||
|
||||
@ -21,17 +21,6 @@ export const CONFIG_IMPORT_PACKAGES = [
|
||||
"@stackframe/template",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* The module specifier from which a given SDK package exposes the
|
||||
* `HexclaveConfig` type and the `defineHexclaveConfig` helper. Hexclave-branded
|
||||
* packages expose them from a lightweight `/config` entrypoint (free of
|
||||
* framework runtime code); legacy `@stackframe/*` releases predate `/config`
|
||||
* and expose them from the package root.
|
||||
*/
|
||||
export function configImportSpecifierForPackage(pkg: string): string {
|
||||
return pkg.startsWith("@hexclave/") ? `${pkg}/config` : pkg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of dependency names (from package.json), returns the SDK
|
||||
* package that should be used for the `HexclaveConfig` import, or `undefined`
|
||||
@ -64,8 +53,7 @@ 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 importSpecifier = configImportSpecifierForPackage(pkg);
|
||||
const importLine = `import type { HexclaveConfig } from "${importSpecifier}";`;
|
||||
const importLine = `import type { HexclaveConfig } from "${pkg}";`;
|
||||
return `${importLine}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
|
||||
}
|
||||
|
||||
@ -102,15 +90,15 @@ 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/config";');
|
||||
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/next";');
|
||||
});
|
||||
|
||||
import.meta.vitest?.test("renderConfigFileContent defaults to @hexclave/js", ({ expect }) => {
|
||||
const content = renderConfigFileContent({});
|
||||
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js/config";');
|
||||
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
|
||||
});
|
||||
|
||||
import.meta.vitest?.test("renderConfigFileContent keeps legacy @stackframe packages on their root entrypoint", ({ expect }) => {
|
||||
import.meta.vitest?.test("renderConfigFileContent imports from the SDK package root", ({ expect }) => {
|
||||
const content = renderConfigFileContent({}, "@stackframe/next");
|
||||
expect(content).toContain('import type { HexclaveConfig } from "@stackframe/next";');
|
||||
});
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
// 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.
|
||||
// Retained only for backwards compatibility with config files that still import
|
||||
// from `<pkg>/config` (e.g. `@hexclave/next/config`). New config files should
|
||||
// import `defineHexclaveConfig` and the config types from the SDK package root
|
||||
// (e.g. `@hexclave/next`) — the SDK roots are side-effect-free enough to load in
|
||||
// a plain Node context (as the local dashboard and CLI do), so the separate
|
||||
// `/config` entrypoint is no longer necessary and is not recommended anymore.
|
||||
//
|
||||
// Hexclave aliases and legacy Stack* names — @deprecated JSDoc lives on the
|
||||
// original declarations in @hexclave/shared/config so it survives dts bundling
|
||||
|
||||
Loading…
Reference in New Issue
Block a user