import { existsSync, readFileSync } from "fs"; import path from "path"; import { parseStackConfigFileContent, renderConfigFileContent } from "./stack-config-file"; export { parseStackConfigFileContent, renderConfigFileContent }; /** * Packages that export the `StackConfig` type, in priority order. * The first match found in a project's dependencies wins. */ const STACKFRAME_CONFIG_PACKAGES = [ "@stackframe/stack", "@stackframe/react", "@stackframe/js", "@stackframe/template", ] as const; /** * Given a list of dependency names (from package.json), returns the * `@stackframe/*` package that should be used for the `StackConfig` import, * or `undefined` if none of the known packages are installed. */ export function detectStackframeImportPackage(dependencies: string[]): string | undefined { for (const pkg of STACKFRAME_CONFIG_PACKAGES) { if (dependencies.includes(pkg)) { return pkg; } } return undefined; } /** * Walks up from `dir` to find the nearest `package.json` and returns the * best `@stackframe/*` package to use for the `StackConfig` type import. */ export function detectImportPackageFromDir(dir: string): string | undefined { let current = dir; while (true) { const pkgPath = path.join(current, "package.json"); if (existsSync(pkgPath)) { try { const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); const deps = [ ...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {}), ]; return detectStackframeImportPackage(deps); } catch { return undefined; } } const parent = path.dirname(current); if (parent === current) break; current = parent; } return undefined; } import.meta.vitest?.test("renderConfigFileContent normalizes config exports", ({ expect }) => { expect(renderConfigFileContent({ "payments.items.todos.displayName": "Todo Slots", "payments.items.todos.customerType": "user", })).toContain(`export const config: StackConfig = { "payments": { "items": { "todos": { "displayName": "Todo Slots", "customerType": "user" } } } };`); }); import.meta.vitest?.test("parseStackConfigFileContent parses static config exports", ({ expect }) => { expect(parseStackConfigFileContent(` import type { StackConfig } from "@stackframe/js"; export const config: StackConfig = { auth: { allowSignUp: true }, payments: { testMode: false }, }; `, "stack.config.ts")).toMatchInlineSnapshot(` { "auth": { "allowSignUp": true, }, "payments": { "testMode": false, }, } `); }); import.meta.vitest?.test("parseStackConfigFileContent parses show-onboarding", ({ expect }) => { expect(parseStackConfigFileContent('export const config = "show-onboarding";', "stack.config.ts")).toBe("show-onboarding"); }); import.meta.vitest?.test("parseStackConfigFileContent rejects dynamic config exports", ({ expect }) => { expect(() => parseStackConfigFileContent("export const config = makeConfig();", "stack.config.ts")).toThrow(/Unsupported config expression/); }); import.meta.vitest?.test("renderConfigFileContent rejects conflicting dotted keys", ({ expect }) => { expect(() => renderConfigFileContent({ "a.b": 1, "a.b.c": 2, })).toThrowError(/conflicting keys.*"a\.b\.c"/); }); import.meta.vitest?.test("renderConfigFileContent rejects invalid config exports", ({ expect }) => { expect(() => renderConfigFileContent(null)).toThrowErrorMatchingInlineSnapshot( `[Error: Invalid config: expected a plain object.]`, ); }); import.meta.vitest?.test("renderConfigFileContent uses custom import package", ({ expect }) => { const content = renderConfigFileContent({}, "@stackframe/stack"); expect(content).toContain('import type { StackConfig } from "@stackframe/stack";'); }); import.meta.vitest?.test("renderConfigFileContent defaults to @stackframe/js", ({ expect }) => { const content = renderConfigFileContent({}); expect(content).toContain('import type { StackConfig } from "@stackframe/js";'); }); import.meta.vitest?.test("detectStackframeImportPackage picks first matching package by priority", ({ expect }) => { expect(detectStackframeImportPackage(["@stackframe/stack", "@stackframe/js"])).toBe("@stackframe/stack"); expect(detectStackframeImportPackage(["@stackframe/react", "@stackframe/js"])).toBe("@stackframe/react"); expect(detectStackframeImportPackage(["@stackframe/js"])).toBe("@stackframe/js"); expect(detectStackframeImportPackage(["@stackframe/template"])).toBe("@stackframe/template"); expect(detectStackframeImportPackage(["lodash", "express"])).toBeUndefined(); expect(detectStackframeImportPackage([])).toBeUndefined(); });