mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
--config-file is now a file, not a folder
This commit is contained in:
parent
9102b3db75
commit
049c557a06
@ -457,3 +457,6 @@ A: `docs-mintlify/apps-sidebar-filter.js` injects the Apps filter with inline st
|
||||
|
||||
## Q: How should `StackAssertionError` preserve an underlying thrown error?
|
||||
A: Pass the underlying error as the `cause` property in the second argument. The `StackAssertionError` constructor only forwards `cause` into `ErrorOptions`, so storing a caught error under an `error` property captures it as ordinary metadata instead of preserving the error cause chain.
|
||||
|
||||
## Q: How should Stack CLI `--config-file` options interpret paths?
|
||||
A: `--config-file` should point directly to a regular config file. Do not treat an existing directory as a shortcut for `stack.config.ts` inside it; reject directories with a clear error instead. `stack config pull` may default to `./stack.config.ts` when the flag is omitted, but an explicitly provided directory is still invalid.
|
||||
|
||||
@ -20,12 +20,21 @@ describe("resolveConfigFilePathForPull", () => {
|
||||
expect(resolveConfigFilePathForPull({ configFile: explicit }, tmpDir)).toBe(path.resolve(explicit));
|
||||
});
|
||||
|
||||
it("rejects an explicit --config-file path that points to a directory", () => {
|
||||
expect(() => resolveConfigFilePathForPull({ configFile: tmpDir }, tmpDir)).toThrow(/must point to a config file/);
|
||||
});
|
||||
|
||||
it("falls back to ./stack.config.ts in cwd when --config-file is omitted", () => {
|
||||
const expected = path.join(tmpDir, "stack.config.ts");
|
||||
fs.writeFileSync(expected, "// placeholder\n");
|
||||
expect(resolveConfigFilePathForPull({}, tmpDir)).toBe(expected);
|
||||
});
|
||||
|
||||
it("rejects the default ./stack.config.ts path when it is a directory", () => {
|
||||
fs.mkdirSync(path.join(tmpDir, "stack.config.ts"));
|
||||
expect(() => resolveConfigFilePathForPull({}, tmpDir)).toThrow(/directory instead of a file/);
|
||||
});
|
||||
|
||||
it("treats an empty --config-file string as omitted (falls back to cwd)", () => {
|
||||
const expected = path.join(tmpDir, "stack.config.ts");
|
||||
fs.writeFileSync(expected, "// placeholder\n");
|
||||
|
||||
@ -4,6 +4,7 @@ import * as fs from "fs";
|
||||
import { isProjectAuthWithRefreshToken, isProjectAuthWithSecretServerKey, resolveAuth, type ProjectAuthWithSecretServerKey } from "../lib/auth.js";
|
||||
import { getAdminProject } from "../lib/app.js";
|
||||
import { CliError } from "../lib/errors.js";
|
||||
import { resolveConfigFilePathOption } from "../lib/config-file-path.js";
|
||||
import type { EnvironmentConfigOverrideOverride } from "@stackframe/stack-shared/dist/config/schema";
|
||||
import { detectImportPackageFromDir, renderConfigFileContent } from "@stackframe/stack-shared/dist/config-rendering";
|
||||
|
||||
@ -121,12 +122,15 @@ function sourceToSdkSource(source: BranchConfigSourceApi):
|
||||
// if it isn't there. Exported for unit tests.
|
||||
export function resolveConfigFilePathForPull(opts: { configFile?: string }, cwd: string): string {
|
||||
if (opts.configFile != null && opts.configFile !== "") {
|
||||
return path.resolve(opts.configFile);
|
||||
return resolveConfigFilePathOption(opts.configFile);
|
||||
}
|
||||
const candidate = path.join(cwd, "stack.config.ts");
|
||||
if (!fs.existsSync(candidate)) {
|
||||
throw new CliError("No --config-file provided and no stack.config.ts found in the current directory. Pass --config-file <path> or run this command in a directory containing a stack.config.ts file.");
|
||||
}
|
||||
if (fs.statSync(candidate).isDirectory()) {
|
||||
throw new CliError(`Default config path points to a directory instead of a file: ${candidate}`);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
@ -175,17 +179,13 @@ export function registerConfigCommand(program: Command) {
|
||||
.action(async (opts) => {
|
||||
const auth = resolveAuth(opts.cloudProjectId);
|
||||
|
||||
const filePath = path.resolve(opts.configFile);
|
||||
const filePath = resolveConfigFilePathOption(opts.configFile, { mustExist: true });
|
||||
const ext = path.extname(filePath);
|
||||
|
||||
if (ext !== ".js" && ext !== ".ts") {
|
||||
throw new CliError("Config file must have a .js or .ts extension.");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new CliError(`Config file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
const { createJiti } = await import("jiti");
|
||||
const jiti = createJiti(import.meta.url);
|
||||
const configModule: { config?: unknown } = await jiti.import(filePath);
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
pollInternalPck,
|
||||
} from "../lib/emulator-paths.js";
|
||||
import { CliError } from "../lib/errors.js";
|
||||
import { resolveConfigFilePathOption } from "../lib/config-file-path.js";
|
||||
import { writeIso } from "../lib/iso.js";
|
||||
|
||||
const DEFAULT_PORT_PREFIX = "81";
|
||||
@ -739,10 +740,7 @@ export function registerEmulatorCommand(program: Command) {
|
||||
|
||||
let resolvedConfigFile: string | undefined;
|
||||
if (opts.configFile) {
|
||||
resolvedConfigFile = resolve(opts.configFile);
|
||||
if (!existsSync(resolvedConfigFile)) {
|
||||
throw new CliError(`Config file not found: ${resolvedConfigFile}`);
|
||||
}
|
||||
resolvedConfigFile = resolveConfigFilePathOption(opts.configFile, { mustExist: true });
|
||||
}
|
||||
|
||||
let freshlyStarted = false;
|
||||
@ -782,10 +780,7 @@ export function registerEmulatorCommand(program: Command) {
|
||||
|
||||
let resolvedConfigFile: string | undefined;
|
||||
if (opts.configFile) {
|
||||
resolvedConfigFile = resolve(opts.configFile);
|
||||
if (!existsSync(resolvedConfigFile)) {
|
||||
throw new CliError(`Config file not found: ${resolvedConfigFile}`);
|
||||
}
|
||||
resolvedConfigFile = resolveConfigFilePathOption(opts.configFile, { mustExist: true });
|
||||
}
|
||||
|
||||
const alreadyRunning = isEmulatorRunning();
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { Command } from "commander";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { isProjectAuthWithRefreshToken, resolveAuth, resolveLocalEmulatorAuth, type ProjectAuthWithRefreshToken } from "../lib/auth.js";
|
||||
import { lookupLocalEmulatorProjectIdByPath } from "../lib/local-emulator-client.js";
|
||||
import { getAdminProject } from "../lib/app.js";
|
||||
import { CliError } from "../lib/errors.js";
|
||||
import { resolveConfigFilePathOption } from "../lib/config-file-path.js";
|
||||
|
||||
function getErrorMessage(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
@ -69,10 +68,7 @@ export function registerExecCommand(program: Command) {
|
||||
}
|
||||
auth = cloudAuth;
|
||||
} else {
|
||||
const absPath = path.resolve(target.configFile);
|
||||
if (!fs.existsSync(absPath)) {
|
||||
throw new CliError(`Config file not found: ${absPath}`);
|
||||
}
|
||||
const absPath = resolveConfigFilePathOption(target.configFile, { mustExist: true });
|
||||
const projectId = await lookupLocalEmulatorProjectIdByPath(absPath);
|
||||
auth = await resolveLocalEmulatorAuth(projectId);
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import { isNonInteractiveEnv } from "../lib/interactive.js";
|
||||
import { createInitPrompt } from "../lib/init-prompt.js";
|
||||
import { createProjectInteractively } from "../lib/create-project.js";
|
||||
import { runClaudeAgent } from "../lib/claude-agent.js";
|
||||
import { resolveConfigFilePathOption } from "../lib/config-file-path.js";
|
||||
import { isEmulatorImageInstalled } from "./emulator.js";
|
||||
import { detectImportPackageFromDir, renderConfigFileContent } from "@stackframe/stack-shared/dist/config-rendering";
|
||||
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
@ -196,14 +197,14 @@ async function handleLinkFromConfigFile(opts: InitOptions): Promise<{ configPath
|
||||
if (!fs.existsSync(resolved)) {
|
||||
return `File not found: ${resolved}`;
|
||||
}
|
||||
if (fs.statSync(resolved).isDirectory()) {
|
||||
return `--config-file must point to a config file, but got a directory: ${resolved}`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const configPath = path.resolve(filePath);
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new CliError(`File not found: ${configPath}`);
|
||||
}
|
||||
const configPath = resolveConfigFilePathOption(filePath, { mustExist: true });
|
||||
|
||||
console.log(`\nLinked to config file: ${configPath}`);
|
||||
return { configPath };
|
||||
|
||||
40
packages/stack-cli/src/lib/config-file-path.test.ts
Normal file
40
packages/stack-cli/src/lib/config-file-path.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { resolveConfigFilePathOption } from "./config-file-path.js";
|
||||
|
||||
describe("resolveConfigFilePathOption", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "stack-cli-config-file-path-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("returns a resolved existing file path", () => {
|
||||
const configFile = path.join(tmpDir, "stack.config.ts");
|
||||
fs.writeFileSync(configFile, "// config\n");
|
||||
|
||||
expect(resolveConfigFilePathOption(configFile, { mustExist: true })).toBe(configFile);
|
||||
});
|
||||
|
||||
it("rejects an existing directory", () => {
|
||||
expect(() => resolveConfigFilePathOption(tmpDir, { mustExist: true })).toThrow(/must point to a config file, but got a directory/);
|
||||
});
|
||||
|
||||
it("allows a missing file path when mustExist is not set", () => {
|
||||
const configFile = path.join(tmpDir, "missing.config.ts");
|
||||
|
||||
expect(resolveConfigFilePathOption(configFile)).toBe(configFile);
|
||||
});
|
||||
|
||||
it("rejects a missing file path when mustExist is set", () => {
|
||||
const configFile = path.join(tmpDir, "missing.config.ts");
|
||||
|
||||
expect(() => resolveConfigFilePathOption(configFile, { mustExist: true })).toThrow(/Config file not found/);
|
||||
});
|
||||
});
|
||||
28
packages/stack-cli/src/lib/config-file-path.ts
Normal file
28
packages/stack-cli/src/lib/config-file-path.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { existsSync, statSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { CliError } from "./errors.js";
|
||||
|
||||
export function resolveConfigFilePathOption(inputPath: string, options?: {
|
||||
mustExist?: boolean,
|
||||
optionName?: string,
|
||||
}): string {
|
||||
const resolved = resolve(inputPath);
|
||||
const optionName = options?.optionName ?? "--config-file";
|
||||
|
||||
if (!existsSync(resolved)) {
|
||||
if (options?.mustExist === true) {
|
||||
throw new CliError(`Config file not found: ${resolved}`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const stat = statSync(resolved);
|
||||
if (stat.isDirectory()) {
|
||||
throw new CliError(`${optionName} must point to a config file, but got a directory: ${resolved}`);
|
||||
}
|
||||
if (!stat.isFile()) {
|
||||
throw new CliError(`${optionName} must point to a regular config file: ${resolved}`);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user