mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
Fix config push failing when SDK isn't installed in target repo
Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
parent
ce956a0fd2
commit
0b2d475975
@ -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, buildConfigPushSource, resolveConfigFilePathForPull } from "./config-file.js";
|
||||
import { assertConfigPullTarget, buildConfigImportAlias, buildConfigPushSource, resolveConfigFilePathForPull } from "./config-file.js";
|
||||
|
||||
describe("resolveConfigFilePathForPull", () => {
|
||||
let tmpDir: string;
|
||||
@ -80,6 +80,60 @@ 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 CLI never depends on `@hexclave/next`, so its `/config` entrypoint is
|
||||
// never resolvable from the config file's directory and must be aliased.
|
||||
expect(alias["@hexclave/next/config"]).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,10 +1,12 @@
|
||||
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";
|
||||
@ -233,6 +235,35 @@ 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")
|
||||
@ -288,7 +319,9 @@ export function registerConfigCommand(program: Command) {
|
||||
}
|
||||
|
||||
const { createJiti } = await import("jiti");
|
||||
const jiti = createJiti(import.meta.url);
|
||||
const jiti = createJiti(import.meta.url, {
|
||||
alias: buildConfigImportAlias(path.dirname(filePath)),
|
||||
});
|
||||
const configModule: { config?: unknown } = await jiti.import(filePath);
|
||||
|
||||
const config = parseConfigOverride(configModule.config);
|
||||
|
||||
@ -9,7 +9,7 @@ const DEFAULT_CONFIG_IMPORT_PACKAGE = "@hexclave/js";
|
||||
* so projects pinned to the last legacy release still render a config file
|
||||
* that compiles against their installed SDK.
|
||||
*/
|
||||
const CONFIG_IMPORT_PACKAGES = [
|
||||
export const CONFIG_IMPORT_PACKAGES = [
|
||||
"@hexclave/next",
|
||||
"@hexclave/react",
|
||||
"@hexclave/tanstack-start",
|
||||
@ -21,6 +21,17 @@ 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`
|
||||
@ -53,7 +64,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 = pkg.startsWith("@hexclave/") ? `${pkg}/config` : pkg;
|
||||
const importSpecifier = configImportSpecifierForPackage(pkg);
|
||||
const importLine = `import type { HexclaveConfig } from "${importSpecifier}";`;
|
||||
return `${importLine}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user