mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
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:
parent
d9a44b51c7
commit
024e511c7f
@ -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;
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user