fix: address PR review comments - computed props, TS assertions, type casts, DRY imports

- Add prop.computed check in evaluateLiteralNode to reject dynamic keys
- Unwrap TSAsExpression/TSSatisfiesExpression so 'satisfies T' and 'as const' resolve
- Remove 'as' type casts, add isRecord type guard for proper narrowing
- DRY up CONFIG_IMPORT_PACKAGES: config-eval.ts now reuses detectConfigImportPackage
- Add tests for computed property rejection and TS assertion unwrapping

Co-Authored-By: mantra <mantra@stack-auth.com>
This commit is contained in:
Devin AI 2026-06-24 02:26:18 +00:00
parent d9a44b51c7
commit 024e511c7f
2 changed files with 27 additions and 29 deletions

View File

@ -1,30 +1,12 @@
import { existsSync, readFileSync } from "fs";
import { createJiti } from "jiti";
import path from "path";
import { detectConfigImportPackage } from "./config-rendering";
export { hexclaveConfigFileExportsConfig } from "./hexclave-config-file";
const jiti = createJiti(import.meta.url, { moduleCache: false });
/**
* 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;
/**
* Walks up from `dir` to find the nearest `package.json` and returns the
* best SDK package to use for the `HexclaveConfig` type import.
@ -40,12 +22,7 @@ export function detectImportPackageFromDir(dir: string): string | undefined {
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.devDependencies ?? {}),
];
for (const known of CONFIG_IMPORT_PACKAGES) {
if (deps.includes(known)) {
return known;
}
}
return undefined;
return detectConfigImportPackage(deps);
} catch {
return undefined;
}

View File

@ -60,6 +60,10 @@ export function renderConfigFileContent(config: unknown, importPackage?: string)
return `${importLine}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === "object" && !Array.isArray(value);
}
/**
* Statically evaluates a Babel AST node representing a JSON-like literal
* (objects, arrays, strings, numbers, booleans, null). Returns `undefined`
@ -67,10 +71,15 @@ export function renderConfigFileContent(config: unknown, importPackage?: string)
* identifiers, template literals, etc.).
*/
function evaluateLiteralNode(node: t.Node): unknown {
// Unwrap TS type assertions so `{ ... } satisfies T` / `{ ... } as const` resolve.
if (t.isTSAsExpression(node) || t.isTSSatisfiesExpression(node)) {
return evaluateLiteralNode(node.expression);
}
if (t.isObjectExpression(node)) {
const result: Record<string, unknown> = {};
for (const prop of node.properties) {
if (t.isSpreadElement(prop) || !t.isObjectProperty(prop)) return undefined;
if (prop.computed) return undefined;
const key = t.isIdentifier(prop.key)
? prop.key.name
: t.isStringLiteral(prop.key)
@ -79,7 +88,7 @@ function evaluateLiteralNode(node: t.Node): unknown {
? String(prop.key.value)
: undefined;
if (key === undefined) return undefined;
const value = evaluateLiteralNode(prop.value as t.Expression);
const value = evaluateLiteralNode(prop.value);
if (value === undefined) return undefined;
result[key] = value;
}
@ -136,9 +145,7 @@ export function parseStaticConfigLiteral(content: string): Record<string, unknow
const value = evaluateLiteralNode(decl.init);
if (value === undefined) return null;
if (typeof value === "string") return value;
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
if (isRecord(value)) return value;
return null;
}
}
@ -225,3 +232,17 @@ export const config: HexclaveConfig = {
};`;
expect(parseStaticConfigLiteral(content)).toEqual({ teams: { allowClientTeamCreation: false } });
});
import.meta.vitest?.test("parseStaticConfigLiteral rejects computed property keys", ({ expect }) => {
expect(parseStaticConfigLiteral('const key = "a"; export const config = { [key]: true };')).toBeNull();
});
import.meta.vitest?.test("parseStaticConfigLiteral unwraps TS `satisfies` assertion", ({ expect }) => {
const content = `import type { HexclaveConfig } from "@hexclave/next";
export const config = { auth: { allowSignUp: true } } satisfies HexclaveConfig;`;
expect(parseStaticConfigLiteral(content)).toEqual({ auth: { allowSignUp: true } });
});
import.meta.vitest?.test("parseStaticConfigLiteral unwraps TS `as const` assertion", ({ expect }) => {
expect(parseStaticConfigLiteral('export const config = { enabled: true } as const;')).toEqual({ enabled: true });
});