@hexclave/shared 1.0.36 → 1.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/unified-prompts/reminders.js +2 -1
- package/dist/ai/unified-prompts/reminders.js.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +2 -2
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
- package/dist/config/format.js +10 -0
- package/dist/config/format.js.map +1 -1
- package/dist/config/schema.d.ts +147 -147
- package/dist/config-eval.d.ts +31 -0
- package/dist/config-eval.d.ts.map +1 -0
- package/dist/config-eval.js +117 -0
- package/dist/config-eval.js.map +1 -0
- package/dist/config-rendering.d.ts +3 -6
- package/dist/config-rendering.d.ts.map +1 -1
- package/dist/config-rendering.js +21 -69
- package/dist/config-rendering.js.map +1 -1
- package/dist/esm/ai/unified-prompts/reminders.js +2 -1
- package/dist/esm/ai/unified-prompts/reminders.js.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +2 -2
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
- package/dist/esm/config/format.js +10 -0
- package/dist/esm/config/format.js.map +1 -1
- package/dist/esm/config/schema.d.ts +147 -147
- package/dist/esm/config-eval.d.ts +31 -0
- package/dist/esm/config-eval.d.ts.map +1 -0
- package/dist/esm/config-eval.js +112 -0
- package/dist/esm/config-eval.js.map +1 -0
- package/dist/esm/config-rendering.d.ts +3 -6
- package/dist/esm/config-rendering.d.ts.map +1 -1
- package/dist/esm/config-rendering.js +15 -58
- package/dist/esm/config-rendering.js.map +1 -1
- package/dist/esm/hooks/use-async-callback.js +1 -1
- package/dist/esm/hooks/use-async-external-store.js +1 -1
- package/dist/esm/hooks/use-strict-memo.js +1 -1
- package/dist/esm/interface/admin-interface.d.ts +44 -2
- package/dist/esm/interface/admin-interface.d.ts.map +1 -1
- package/dist/esm/interface/admin-interface.js +55 -0
- package/dist/esm/interface/admin-interface.js.map +1 -1
- package/dist/esm/interface/admin-metrics.d.ts +12 -12
- package/dist/esm/interface/conversations.d.ts +31 -31
- package/dist/esm/interface/crud/current-user.d.ts +7 -7
- package/dist/esm/interface/crud/email-outbox.d.ts +158 -158
- package/dist/esm/interface/crud/invoices.d.ts +2 -2
- package/dist/esm/interface/crud/products.d.ts +15 -15
- package/dist/esm/interface/crud/products.d.ts.map +1 -1
- package/dist/esm/interface/crud/project-api-keys.d.ts +2 -2
- package/dist/esm/interface/crud/team-member-profiles.d.ts +18 -18
- package/dist/esm/interface/crud/transactions.d.ts +20 -20
- package/dist/esm/interface/crud/transactions.d.ts.map +1 -1
- package/dist/esm/interface/crud/users.d.ts +12 -12
- package/dist/esm/interface/plan-usage.d.ts +2 -2
- package/dist/esm/interface/webhooks.d.ts +2 -2
- package/dist/esm/known-errors.d.ts +1 -1
- package/dist/esm/schema-fields.d.ts +35 -2
- package/dist/esm/schema-fields.d.ts.map +1 -1
- package/dist/esm/schema-fields.js +53 -2
- package/dist/esm/schema-fields.js.map +1 -1
- package/dist/esm/sessions.d.ts +7 -7
- package/dist/esm/utils/jsx-editable-transpiler.js +2 -2
- package/dist/hooks/use-async-callback.js +1 -1
- package/dist/hooks/use-async-external-store.js +1 -1
- package/dist/hooks/use-strict-memo.js +1 -1
- package/dist/interface/admin-interface.d.ts +44 -2
- package/dist/interface/admin-interface.d.ts.map +1 -1
- package/dist/interface/admin-interface.js +55 -0
- package/dist/interface/admin-interface.js.map +1 -1
- package/dist/interface/admin-metrics.d.ts +12 -12
- package/dist/interface/conversations.d.ts +31 -31
- package/dist/interface/crud/current-user.d.ts +7 -7
- package/dist/interface/crud/email-outbox.d.ts +158 -158
- package/dist/interface/crud/invoices.d.ts +2 -2
- package/dist/interface/crud/products.d.ts +15 -15
- package/dist/interface/crud/products.d.ts.map +1 -1
- package/dist/interface/crud/project-api-keys.d.ts +2 -2
- package/dist/interface/crud/team-member-profiles.d.ts +18 -18
- package/dist/interface/crud/transactions.d.ts +20 -20
- package/dist/interface/crud/transactions.d.ts.map +1 -1
- package/dist/interface/crud/users.d.ts +12 -12
- package/dist/interface/plan-usage.d.ts +2 -2
- package/dist/interface/webhooks.d.ts +2 -2
- package/dist/known-errors.d.ts +1 -1
- package/dist/schema-fields.d.ts +35 -2
- package/dist/schema-fields.d.ts.map +1 -1
- package/dist/schema-fields.js +55 -1
- package/dist/schema-fields.js.map +1 -1
- package/dist/sessions.d.ts +7 -7
- package/dist/utils/jsx-editable-transpiler.js +4 -4
- package/package.json +2 -1
- package/src/ai/unified-prompts/reminders.ts +2 -1
- package/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts +2 -2
- package/src/config/format.ts +18 -0
- package/src/config-eval.ts +140 -0
- package/src/config-rendering.ts +23 -79
- package/src/interface/admin-interface.ts +85 -5
- package/src/schema-fields.ts +59 -1
- package/dist/esm/hexclave-config-file.d.ts +0 -38
- package/dist/esm/hexclave-config-file.d.ts.map +0 -1
- package/dist/esm/hexclave-config-file.js +0 -144
- package/dist/esm/hexclave-config-file.js.map +0 -1
- package/dist/hexclave-config-file.d.ts +0 -38
- package/dist/hexclave-config-file.d.ts.map +0 -1
- package/dist/hexclave-config-file.js +0 -153
- package/dist/hexclave-config-file.js.map +0 -1
- package/src/hexclave-config-file.ts +0 -215
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import * as parser from "@babel/parser";
|
|
2
|
-
import * as t from "@babel/types";
|
|
3
|
-
import { isValidConfig, normalize } from "./config/format.js";
|
|
4
|
-
|
|
5
|
-
//#region src/hexclave-config-file.ts
|
|
6
|
-
const showOnboardingHexclaveConfigValue = "show-onboarding";
|
|
7
|
-
const DEFAULT_CONFIG_IMPORT_PACKAGE = "@hexclave/js";
|
|
8
|
-
/**
|
|
9
|
-
* Renders a config object into the source text of a `stack.config.ts` file.
|
|
10
|
-
*
|
|
11
|
-
* Browser-safe: kept here (next to `parseHexclaveConfigFileContent`) instead of in
|
|
12
|
-
* `config-rendering.ts` so dashboard client code can render config files
|
|
13
|
-
* without pulling in `fs` / `path`.
|
|
14
|
-
*/
|
|
15
|
-
function renderConfigFileContent(config, importPackage) {
|
|
16
|
-
if (!isValidConfig(config)) throw new Error("Invalid config: expected a plain object.");
|
|
17
|
-
const droppedKeys = [];
|
|
18
|
-
const normalizedConfig = normalize(config, {
|
|
19
|
-
onDotIntoNonObject: "ignore",
|
|
20
|
-
onDotIntoNull: "empty-object",
|
|
21
|
-
droppedKeys
|
|
22
|
-
});
|
|
23
|
-
if (droppedKeys.length > 0) throw new Error(`Config has conflicting keys that would be dropped during normalization: ${droppedKeys.map((k) => JSON.stringify(k)).join(", ")}`);
|
|
24
|
-
const pkg = importPackage ?? DEFAULT_CONFIG_IMPORT_PACKAGE;
|
|
25
|
-
return `${`import type { HexclaveConfig } from "${pkg.startsWith("@hexclave/") ? `${pkg}/config` : pkg}";`}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
|
|
26
|
-
}
|
|
27
|
-
function unwrapStaticConfigExpression(expression) {
|
|
28
|
-
if (t.isTSAsExpression(expression) || t.isTSSatisfiesExpression(expression) || t.isTSTypeAssertion(expression) || t.isTSNonNullExpression(expression)) return unwrapStaticConfigExpression(expression.expression);
|
|
29
|
-
return expression;
|
|
30
|
-
}
|
|
31
|
-
function evaluateStaticConfigExpression(expression) {
|
|
32
|
-
const unwrapped = unwrapStaticConfigExpression(expression);
|
|
33
|
-
if (t.isStringLiteral(unwrapped)) return unwrapped.value;
|
|
34
|
-
if (t.isBooleanLiteral(unwrapped)) return unwrapped.value;
|
|
35
|
-
if (t.isNumericLiteral(unwrapped)) return unwrapped.value;
|
|
36
|
-
if (t.isNullLiteral(unwrapped)) return null;
|
|
37
|
-
if (t.isIdentifier(unwrapped) && unwrapped.name === "undefined") return void 0;
|
|
38
|
-
if (t.isUnaryExpression(unwrapped) && unwrapped.operator === "-" && t.isNumericLiteral(unwrapped.argument)) return -unwrapped.argument.value;
|
|
39
|
-
if (t.isArrayExpression(unwrapped)) return unwrapped.elements.map((element) => {
|
|
40
|
-
if (element == null || t.isSpreadElement(element)) throw new Error("Config arrays cannot contain holes or spreads.");
|
|
41
|
-
return evaluateStaticConfigExpression(element);
|
|
42
|
-
});
|
|
43
|
-
if (t.isObjectExpression(unwrapped)) {
|
|
44
|
-
const result = {};
|
|
45
|
-
for (const property of unwrapped.properties) {
|
|
46
|
-
if (t.isSpreadElement(property)) throw new Error("Config objects cannot contain spreads.");
|
|
47
|
-
if (property.computed) throw new Error("Config object keys cannot be computed.");
|
|
48
|
-
const key = t.isIdentifier(property.key) ? property.key.name : t.isStringLiteral(property.key) || t.isNumericLiteral(property.key) ? String(property.key.value) : null;
|
|
49
|
-
if (key == null) throw new Error("Unsupported config object key.");
|
|
50
|
-
if (t.isObjectMethod(property)) throw new Error("Config objects cannot contain methods.");
|
|
51
|
-
if (!t.isExpression(property.value)) throw new Error("Unsupported config object value.");
|
|
52
|
-
result[key] = evaluateStaticConfigExpression(property.value);
|
|
53
|
-
}
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
throw new Error(`Unsupported config expression: ${unwrapped.type}`);
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Like {@link parseHexclaveConfigFileContent}, but returns `null` instead of
|
|
60
|
-
* throwing when the file is not a plain static config (e.g. it wraps the config
|
|
61
|
-
* in a helper call, references imported values, or has a syntax error). Useful
|
|
62
|
-
* for deciding whether a config file can be safely regenerated deterministically
|
|
63
|
-
* or whether it has custom structure that must be preserved.
|
|
64
|
-
*/
|
|
65
|
-
function tryParseHexclaveConfigFileContent(content, filePath) {
|
|
66
|
-
try {
|
|
67
|
-
return parseHexclaveConfigFileContent(content, filePath);
|
|
68
|
-
} catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Returns whether `content` parses as a module that exports a `config` binding.
|
|
74
|
-
* Used as a lightweight structural sanity check after editing config files whose
|
|
75
|
-
* values can't be evaluated by our loader (e.g. they import external text
|
|
76
|
-
* files), where a full semantic comparison isn't possible.
|
|
77
|
-
*/
|
|
78
|
-
function hexclaveConfigFileExportsConfig(content, filePath) {
|
|
79
|
-
let ast;
|
|
80
|
-
try {
|
|
81
|
-
ast = parser.parse(content, {
|
|
82
|
-
sourceType: "module",
|
|
83
|
-
sourceFilename: filePath,
|
|
84
|
-
plugins: ["typescript", "importAttributes"]
|
|
85
|
-
});
|
|
86
|
-
} catch {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
for (const statement of ast.program.body) {
|
|
90
|
-
if (!t.isExportNamedDeclaration(statement)) continue;
|
|
91
|
-
if (statement.exportKind === "type") continue;
|
|
92
|
-
if (t.isVariableDeclaration(statement.declaration)) {
|
|
93
|
-
for (const declaration of statement.declaration.declarations) if (t.isIdentifier(declaration.id) && declaration.id.name === "config" && declaration.init != null) return true;
|
|
94
|
-
}
|
|
95
|
-
for (const specifier of statement.specifiers) if (t.isExportSpecifier(specifier) && specifier.exportKind !== "type") {
|
|
96
|
-
if ((t.isIdentifier(specifier.exported) ? specifier.exported.name : specifier.exported.value) === "config") return true;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Returns the relative import sources (those starting with `./` or `../`)
|
|
103
|
-
* declared in `content`. Used to discover the external files a config update may
|
|
104
|
-
* touch — e.g. `import x from "./welcome-email.tsx" with { type: "text" }` — so
|
|
105
|
-
* they can be snapshotted and rolled back if an in-place update fails. Returns
|
|
106
|
-
* an empty array if the file can't be parsed.
|
|
107
|
-
*/
|
|
108
|
-
function getRelativeImportSpecifiers(content) {
|
|
109
|
-
let ast;
|
|
110
|
-
try {
|
|
111
|
-
ast = parser.parse(content, {
|
|
112
|
-
sourceType: "module",
|
|
113
|
-
plugins: ["typescript", "importAttributes"]
|
|
114
|
-
});
|
|
115
|
-
} catch {
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
const sources = [];
|
|
119
|
-
for (const statement of ast.program.body) if (t.isImportDeclaration(statement)) {
|
|
120
|
-
const source = statement.source.value;
|
|
121
|
-
if (source.startsWith("./") || source.startsWith("../")) sources.push(source);
|
|
122
|
-
}
|
|
123
|
-
return sources;
|
|
124
|
-
}
|
|
125
|
-
function parseHexclaveConfigFileContent(content, filePath) {
|
|
126
|
-
if (content.trim() === "") return {};
|
|
127
|
-
const ast = parser.parse(content, {
|
|
128
|
-
sourceType: "module",
|
|
129
|
-
plugins: ["typescript", "importAttributes"]
|
|
130
|
-
});
|
|
131
|
-
for (const statement of ast.program.body) {
|
|
132
|
-
if (!t.isExportNamedDeclaration(statement) || !t.isVariableDeclaration(statement.declaration)) continue;
|
|
133
|
-
for (const declaration of statement.declaration.declarations) {
|
|
134
|
-
if (!t.isIdentifier(declaration.id) || declaration.id.name !== "config") continue;
|
|
135
|
-
if (declaration.init == null || !t.isExpression(declaration.init)) throw new Error(`Config export in ${filePath} must have an initializer.`);
|
|
136
|
-
return evaluateStaticConfigExpression(declaration.init);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
throw new Error(`Invalid config in ${filePath}. The file must export a plain \`config\` object or "show-onboarding".`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
//#endregion
|
|
143
|
-
export { getRelativeImportSpecifiers, hexclaveConfigFileExportsConfig, parseHexclaveConfigFileContent, renderConfigFileContent, showOnboardingHexclaveConfigValue, tryParseHexclaveConfigFileContent };
|
|
144
|
-
//# sourceMappingURL=hexclave-config-file.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hexclave-config-file.js","names":[],"sources":["../../src/hexclave-config-file.ts"],"sourcesContent":["import * as parser from \"@babel/parser\";\nimport * as t from \"@babel/types\";\nimport { isValidConfig, normalize } from \"./config/format\";\n\nexport const showOnboardingHexclaveConfigValue = \"show-onboarding\";\n\nconst DEFAULT_CONFIG_IMPORT_PACKAGE = \"@hexclave/js\";\n\n/**\n * Renders a config object into the source text of a `stack.config.ts` file.\n *\n * Browser-safe: kept here (next to `parseHexclaveConfigFileContent`) instead of in\n * `config-rendering.ts` so dashboard client code can render config files\n * without pulling in `fs` / `path`.\n */\nexport function renderConfigFileContent(config: unknown, importPackage?: string): string {\n if (!isValidConfig(config)) {\n throw new Error(\"Invalid config: expected a plain object.\");\n }\n\n const droppedKeys: string[] = [];\n const normalizedConfig = normalize(config, {\n onDotIntoNonObject: \"ignore\",\n onDotIntoNull: \"empty-object\",\n droppedKeys,\n });\n if (droppedKeys.length > 0) {\n throw new Error(`Config has conflicting keys that would be dropped during normalization: ${droppedKeys.map(k => JSON.stringify(k)).join(\", \")}`);\n }\n const pkg = importPackage ?? DEFAULT_CONFIG_IMPORT_PACKAGE;\n // Import the `HexclaveConfig` type from the package's lightweight `/config`\n // entrypoint, which is free of framework runtime code and therefore safe for\n // tooling (e.g. the local dashboard) to load in a plain Node context. Only the\n // Hexclave-branded packages expose this subpath; legacy `@stackframe/*`\n // releases predate it, so fall back to their package root.\n const importSpecifier = pkg.startsWith(\"@hexclave/\") ? `${pkg}/config` : pkg;\n const importLine = `import type { HexclaveConfig } from \"${importSpecifier}\";`;\n return `${importLine}\\n\\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\\n`;\n}\n\ntype ParsedStackConfig = Record<string, unknown> | typeof showOnboardingHexclaveConfigValue;\n\nfunction unwrapStaticConfigExpression(expression: t.Expression): t.Expression {\n if (\n t.isTSAsExpression(expression)\n || t.isTSSatisfiesExpression(expression)\n || t.isTSTypeAssertion(expression)\n || t.isTSNonNullExpression(expression)\n ) {\n return unwrapStaticConfigExpression(expression.expression);\n }\n return expression;\n}\n\nfunction evaluateStaticConfigExpression(expression: t.Expression): unknown {\n const unwrapped = unwrapStaticConfigExpression(expression);\n if (t.isStringLiteral(unwrapped)) return unwrapped.value;\n if (t.isBooleanLiteral(unwrapped)) return unwrapped.value;\n if (t.isNumericLiteral(unwrapped)) return unwrapped.value;\n if (t.isNullLiteral(unwrapped)) return null;\n if (t.isIdentifier(unwrapped) && unwrapped.name === \"undefined\") return undefined;\n if (t.isUnaryExpression(unwrapped) && unwrapped.operator === \"-\" && t.isNumericLiteral(unwrapped.argument)) {\n return -unwrapped.argument.value;\n }\n if (t.isArrayExpression(unwrapped)) {\n return unwrapped.elements.map((element) => {\n if (element == null || t.isSpreadElement(element)) {\n throw new Error(\"Config arrays cannot contain holes or spreads.\");\n }\n return evaluateStaticConfigExpression(element);\n });\n }\n if (t.isObjectExpression(unwrapped)) {\n const result: Record<string, unknown> = {};\n for (const property of unwrapped.properties) {\n if (t.isSpreadElement(property)) {\n throw new Error(\"Config objects cannot contain spreads.\");\n }\n if (property.computed) {\n throw new Error(\"Config object keys cannot be computed.\");\n }\n const key = t.isIdentifier(property.key)\n ? property.key.name\n : t.isStringLiteral(property.key) || t.isNumericLiteral(property.key)\n ? String(property.key.value)\n : null;\n if (key == null) {\n throw new Error(\"Unsupported config object key.\");\n }\n if (t.isObjectMethod(property)) {\n throw new Error(\"Config objects cannot contain methods.\");\n }\n if (!t.isExpression(property.value)) {\n throw new Error(\"Unsupported config object value.\");\n }\n result[key] = evaluateStaticConfigExpression(property.value);\n }\n return result;\n }\n throw new Error(`Unsupported config expression: ${unwrapped.type}`);\n}\n\n/**\n * Like {@link parseHexclaveConfigFileContent}, but returns `null` instead of\n * throwing when the file is not a plain static config (e.g. it wraps the config\n * in a helper call, references imported values, or has a syntax error). Useful\n * for deciding whether a config file can be safely regenerated deterministically\n * or whether it has custom structure that must be preserved.\n */\nexport function tryParseHexclaveConfigFileContent(content: string, filePath: string): ParsedStackConfig | null {\n try {\n return parseHexclaveConfigFileContent(content, filePath);\n } catch {\n return null;\n }\n}\n\n/**\n * Returns whether `content` parses as a module that exports a `config` binding.\n * Used as a lightweight structural sanity check after editing config files whose\n * values can't be evaluated by our loader (e.g. they import external text\n * files), where a full semantic comparison isn't possible.\n */\nexport function hexclaveConfigFileExportsConfig(content: string, filePath: string): boolean {\n let ast: parser.ParseResult<t.File>;\n try {\n ast = parser.parse(content, {\n sourceType: \"module\",\n sourceFilename: filePath,\n plugins: [\"typescript\", \"importAttributes\"],\n });\n } catch {\n return false;\n }\n for (const statement of ast.program.body) {\n if (!t.isExportNamedDeclaration(statement)) {\n continue;\n }\n // Ignore type-only exports (`export type { config }`): they don't produce a\n // runtime `config` value, so they must not satisfy the structural check.\n if (statement.exportKind === \"type\") {\n continue;\n }\n if (t.isVariableDeclaration(statement.declaration)) {\n for (const declaration of statement.declaration.declarations) {\n if (t.isIdentifier(declaration.id) && declaration.id.name === \"config\" && declaration.init != null) {\n return true;\n }\n }\n }\n for (const specifier of statement.specifiers) {\n if (t.isExportSpecifier(specifier) && specifier.exportKind !== \"type\") {\n const exportedName = t.isIdentifier(specifier.exported) ? specifier.exported.name : specifier.exported.value;\n if (exportedName === \"config\") {\n return true;\n }\n }\n }\n }\n return false;\n}\n\n/**\n * Returns the relative import sources (those starting with `./` or `../`)\n * declared in `content`. Used to discover the external files a config update may\n * touch — e.g. `import x from \"./welcome-email.tsx\" with { type: \"text\" }` — so\n * they can be snapshotted and rolled back if an in-place update fails. Returns\n * an empty array if the file can't be parsed.\n */\nexport function getRelativeImportSpecifiers(content: string): string[] {\n let ast: parser.ParseResult<t.File>;\n try {\n ast = parser.parse(content, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"importAttributes\"],\n });\n } catch {\n return [];\n }\n const sources: string[] = [];\n for (const statement of ast.program.body) {\n if (t.isImportDeclaration(statement)) {\n const source = statement.source.value;\n if (source.startsWith(\"./\") || source.startsWith(\"../\")) {\n sources.push(source);\n }\n }\n }\n return sources;\n}\n\nexport function parseHexclaveConfigFileContent(content: string, filePath: string): ParsedStackConfig {\n if (content.trim() === \"\") return {};\n const ast = parser.parse(content, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"importAttributes\"],\n });\n\n for (const statement of ast.program.body) {\n if (!t.isExportNamedDeclaration(statement) || !t.isVariableDeclaration(statement.declaration)) {\n continue;\n }\n for (const declaration of statement.declaration.declarations) {\n if (!t.isIdentifier(declaration.id) || declaration.id.name !== \"config\") {\n continue;\n }\n if (declaration.init == null || !t.isExpression(declaration.init)) {\n throw new Error(`Config export in ${filePath} must have an initializer.`);\n }\n return evaluateStaticConfigExpression(declaration.init) as ParsedStackConfig;\n }\n }\n\n throw new Error(`Invalid config in ${filePath}. The file must export a plain \\`config\\` object or \"show-onboarding\".`);\n}\n"],"mappings":"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"}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
//#region src/hexclave-config-file.d.ts
|
|
2
|
-
declare const showOnboardingHexclaveConfigValue = "show-onboarding";
|
|
3
|
-
/**
|
|
4
|
-
* Renders a config object into the source text of a `stack.config.ts` file.
|
|
5
|
-
*
|
|
6
|
-
* Browser-safe: kept here (next to `parseHexclaveConfigFileContent`) instead of in
|
|
7
|
-
* `config-rendering.ts` so dashboard client code can render config files
|
|
8
|
-
* without pulling in `fs` / `path`.
|
|
9
|
-
*/
|
|
10
|
-
declare function renderConfigFileContent(config: unknown, importPackage?: string): string;
|
|
11
|
-
type ParsedStackConfig = Record<string, unknown> | typeof showOnboardingHexclaveConfigValue;
|
|
12
|
-
/**
|
|
13
|
-
* Like {@link parseHexclaveConfigFileContent}, but returns `null` instead of
|
|
14
|
-
* throwing when the file is not a plain static config (e.g. it wraps the config
|
|
15
|
-
* in a helper call, references imported values, or has a syntax error). Useful
|
|
16
|
-
* for deciding whether a config file can be safely regenerated deterministically
|
|
17
|
-
* or whether it has custom structure that must be preserved.
|
|
18
|
-
*/
|
|
19
|
-
declare function tryParseHexclaveConfigFileContent(content: string, filePath: string): ParsedStackConfig | null;
|
|
20
|
-
/**
|
|
21
|
-
* Returns whether `content` parses as a module that exports a `config` binding.
|
|
22
|
-
* Used as a lightweight structural sanity check after editing config files whose
|
|
23
|
-
* values can't be evaluated by our loader (e.g. they import external text
|
|
24
|
-
* files), where a full semantic comparison isn't possible.
|
|
25
|
-
*/
|
|
26
|
-
declare function hexclaveConfigFileExportsConfig(content: string, filePath: string): boolean;
|
|
27
|
-
/**
|
|
28
|
-
* Returns the relative import sources (those starting with `./` or `../`)
|
|
29
|
-
* declared in `content`. Used to discover the external files a config update may
|
|
30
|
-
* touch — e.g. `import x from "./welcome-email.tsx" with { type: "text" }` — so
|
|
31
|
-
* they can be snapshotted and rolled back if an in-place update fails. Returns
|
|
32
|
-
* an empty array if the file can't be parsed.
|
|
33
|
-
*/
|
|
34
|
-
declare function getRelativeImportSpecifiers(content: string): string[];
|
|
35
|
-
declare function parseHexclaveConfigFileContent(content: string, filePath: string): ParsedStackConfig;
|
|
36
|
-
//#endregion
|
|
37
|
-
export { getRelativeImportSpecifiers, hexclaveConfigFileExportsConfig, parseHexclaveConfigFileContent, renderConfigFileContent, showOnboardingHexclaveConfigValue, tryParseHexclaveConfigFileContent };
|
|
38
|
-
//# sourceMappingURL=hexclave-config-file.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hexclave-config-file.d.ts","names":[],"sources":["../src/hexclave-config-file.ts"],"mappings":";cAIa,iCAAA;AAAb;;;;;AAWA;;AAXA,iBAWgB,uBAAA,CAAwB,MAAA,WAAiB,aAAA;AAAA,KAyBpD,iBAAA,GAAoB,MAAA,2BAAiC,iCAAA;;AAFzD;;;;;AAuED;iBAAgB,iCAAA,CAAkC,OAAA,UAAiB,QAAA,WAAmB,iBAAA;;;;;;;iBActE,+BAAA,CAAgC,OAAA,UAAiB,QAAA;AAAjE;;;;;AA8CA;;AA9CA,iBA8CgB,2BAAA,CAA4B,OAAA;AAAA,iBAsB5B,8BAAA,CAA+B,OAAA,UAAiB,QAAA,WAAmB,iBAAA"}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_chunk = require('./chunk-BE-pF4vm.js');
|
|
3
|
-
let _babel_parser = require("@babel/parser");
|
|
4
|
-
_babel_parser = require_chunk.__toESM(_babel_parser);
|
|
5
|
-
let _babel_types = require("@babel/types");
|
|
6
|
-
_babel_types = require_chunk.__toESM(_babel_types);
|
|
7
|
-
let __config_format_js = require("./config/format.js");
|
|
8
|
-
|
|
9
|
-
//#region src/hexclave-config-file.ts
|
|
10
|
-
const showOnboardingHexclaveConfigValue = "show-onboarding";
|
|
11
|
-
const DEFAULT_CONFIG_IMPORT_PACKAGE = "@hexclave/js";
|
|
12
|
-
/**
|
|
13
|
-
* Renders a config object into the source text of a `stack.config.ts` file.
|
|
14
|
-
*
|
|
15
|
-
* Browser-safe: kept here (next to `parseHexclaveConfigFileContent`) instead of in
|
|
16
|
-
* `config-rendering.ts` so dashboard client code can render config files
|
|
17
|
-
* without pulling in `fs` / `path`.
|
|
18
|
-
*/
|
|
19
|
-
function renderConfigFileContent(config, importPackage) {
|
|
20
|
-
if (!(0, __config_format_js.isValidConfig)(config)) throw new Error("Invalid config: expected a plain object.");
|
|
21
|
-
const droppedKeys = [];
|
|
22
|
-
const normalizedConfig = (0, __config_format_js.normalize)(config, {
|
|
23
|
-
onDotIntoNonObject: "ignore",
|
|
24
|
-
onDotIntoNull: "empty-object",
|
|
25
|
-
droppedKeys
|
|
26
|
-
});
|
|
27
|
-
if (droppedKeys.length > 0) throw new Error(`Config has conflicting keys that would be dropped during normalization: ${droppedKeys.map((k) => JSON.stringify(k)).join(", ")}`);
|
|
28
|
-
const pkg = importPackage ?? DEFAULT_CONFIG_IMPORT_PACKAGE;
|
|
29
|
-
return `${`import type { HexclaveConfig } from "${pkg.startsWith("@hexclave/") ? `${pkg}/config` : pkg}";`}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
|
|
30
|
-
}
|
|
31
|
-
function unwrapStaticConfigExpression(expression) {
|
|
32
|
-
if (_babel_types.isTSAsExpression(expression) || _babel_types.isTSSatisfiesExpression(expression) || _babel_types.isTSTypeAssertion(expression) || _babel_types.isTSNonNullExpression(expression)) return unwrapStaticConfigExpression(expression.expression);
|
|
33
|
-
return expression;
|
|
34
|
-
}
|
|
35
|
-
function evaluateStaticConfigExpression(expression) {
|
|
36
|
-
const unwrapped = unwrapStaticConfigExpression(expression);
|
|
37
|
-
if (_babel_types.isStringLiteral(unwrapped)) return unwrapped.value;
|
|
38
|
-
if (_babel_types.isBooleanLiteral(unwrapped)) return unwrapped.value;
|
|
39
|
-
if (_babel_types.isNumericLiteral(unwrapped)) return unwrapped.value;
|
|
40
|
-
if (_babel_types.isNullLiteral(unwrapped)) return null;
|
|
41
|
-
if (_babel_types.isIdentifier(unwrapped) && unwrapped.name === "undefined") return void 0;
|
|
42
|
-
if (_babel_types.isUnaryExpression(unwrapped) && unwrapped.operator === "-" && _babel_types.isNumericLiteral(unwrapped.argument)) return -unwrapped.argument.value;
|
|
43
|
-
if (_babel_types.isArrayExpression(unwrapped)) return unwrapped.elements.map((element) => {
|
|
44
|
-
if (element == null || _babel_types.isSpreadElement(element)) throw new Error("Config arrays cannot contain holes or spreads.");
|
|
45
|
-
return evaluateStaticConfigExpression(element);
|
|
46
|
-
});
|
|
47
|
-
if (_babel_types.isObjectExpression(unwrapped)) {
|
|
48
|
-
const result = {};
|
|
49
|
-
for (const property of unwrapped.properties) {
|
|
50
|
-
if (_babel_types.isSpreadElement(property)) throw new Error("Config objects cannot contain spreads.");
|
|
51
|
-
if (property.computed) throw new Error("Config object keys cannot be computed.");
|
|
52
|
-
const key = _babel_types.isIdentifier(property.key) ? property.key.name : _babel_types.isStringLiteral(property.key) || _babel_types.isNumericLiteral(property.key) ? String(property.key.value) : null;
|
|
53
|
-
if (key == null) throw new Error("Unsupported config object key.");
|
|
54
|
-
if (_babel_types.isObjectMethod(property)) throw new Error("Config objects cannot contain methods.");
|
|
55
|
-
if (!_babel_types.isExpression(property.value)) throw new Error("Unsupported config object value.");
|
|
56
|
-
result[key] = evaluateStaticConfigExpression(property.value);
|
|
57
|
-
}
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
throw new Error(`Unsupported config expression: ${unwrapped.type}`);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Like {@link parseHexclaveConfigFileContent}, but returns `null` instead of
|
|
64
|
-
* throwing when the file is not a plain static config (e.g. it wraps the config
|
|
65
|
-
* in a helper call, references imported values, or has a syntax error). Useful
|
|
66
|
-
* for deciding whether a config file can be safely regenerated deterministically
|
|
67
|
-
* or whether it has custom structure that must be preserved.
|
|
68
|
-
*/
|
|
69
|
-
function tryParseHexclaveConfigFileContent(content, filePath) {
|
|
70
|
-
try {
|
|
71
|
-
return parseHexclaveConfigFileContent(content, filePath);
|
|
72
|
-
} catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Returns whether `content` parses as a module that exports a `config` binding.
|
|
78
|
-
* Used as a lightweight structural sanity check after editing config files whose
|
|
79
|
-
* values can't be evaluated by our loader (e.g. they import external text
|
|
80
|
-
* files), where a full semantic comparison isn't possible.
|
|
81
|
-
*/
|
|
82
|
-
function hexclaveConfigFileExportsConfig(content, filePath) {
|
|
83
|
-
let ast;
|
|
84
|
-
try {
|
|
85
|
-
ast = _babel_parser.parse(content, {
|
|
86
|
-
sourceType: "module",
|
|
87
|
-
sourceFilename: filePath,
|
|
88
|
-
plugins: ["typescript", "importAttributes"]
|
|
89
|
-
});
|
|
90
|
-
} catch {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
for (const statement of ast.program.body) {
|
|
94
|
-
if (!_babel_types.isExportNamedDeclaration(statement)) continue;
|
|
95
|
-
if (statement.exportKind === "type") continue;
|
|
96
|
-
if (_babel_types.isVariableDeclaration(statement.declaration)) {
|
|
97
|
-
for (const declaration of statement.declaration.declarations) if (_babel_types.isIdentifier(declaration.id) && declaration.id.name === "config" && declaration.init != null) return true;
|
|
98
|
-
}
|
|
99
|
-
for (const specifier of statement.specifiers) if (_babel_types.isExportSpecifier(specifier) && specifier.exportKind !== "type") {
|
|
100
|
-
if ((_babel_types.isIdentifier(specifier.exported) ? specifier.exported.name : specifier.exported.value) === "config") return true;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Returns the relative import sources (those starting with `./` or `../`)
|
|
107
|
-
* declared in `content`. Used to discover the external files a config update may
|
|
108
|
-
* touch — e.g. `import x from "./welcome-email.tsx" with { type: "text" }` — so
|
|
109
|
-
* they can be snapshotted and rolled back if an in-place update fails. Returns
|
|
110
|
-
* an empty array if the file can't be parsed.
|
|
111
|
-
*/
|
|
112
|
-
function getRelativeImportSpecifiers(content) {
|
|
113
|
-
let ast;
|
|
114
|
-
try {
|
|
115
|
-
ast = _babel_parser.parse(content, {
|
|
116
|
-
sourceType: "module",
|
|
117
|
-
plugins: ["typescript", "importAttributes"]
|
|
118
|
-
});
|
|
119
|
-
} catch {
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
const sources = [];
|
|
123
|
-
for (const statement of ast.program.body) if (_babel_types.isImportDeclaration(statement)) {
|
|
124
|
-
const source = statement.source.value;
|
|
125
|
-
if (source.startsWith("./") || source.startsWith("../")) sources.push(source);
|
|
126
|
-
}
|
|
127
|
-
return sources;
|
|
128
|
-
}
|
|
129
|
-
function parseHexclaveConfigFileContent(content, filePath) {
|
|
130
|
-
if (content.trim() === "") return {};
|
|
131
|
-
const ast = _babel_parser.parse(content, {
|
|
132
|
-
sourceType: "module",
|
|
133
|
-
plugins: ["typescript", "importAttributes"]
|
|
134
|
-
});
|
|
135
|
-
for (const statement of ast.program.body) {
|
|
136
|
-
if (!_babel_types.isExportNamedDeclaration(statement) || !_babel_types.isVariableDeclaration(statement.declaration)) continue;
|
|
137
|
-
for (const declaration of statement.declaration.declarations) {
|
|
138
|
-
if (!_babel_types.isIdentifier(declaration.id) || declaration.id.name !== "config") continue;
|
|
139
|
-
if (declaration.init == null || !_babel_types.isExpression(declaration.init)) throw new Error(`Config export in ${filePath} must have an initializer.`);
|
|
140
|
-
return evaluateStaticConfigExpression(declaration.init);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
throw new Error(`Invalid config in ${filePath}. The file must export a plain \`config\` object or "show-onboarding".`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
//#endregion
|
|
147
|
-
exports.getRelativeImportSpecifiers = getRelativeImportSpecifiers;
|
|
148
|
-
exports.hexclaveConfigFileExportsConfig = hexclaveConfigFileExportsConfig;
|
|
149
|
-
exports.parseHexclaveConfigFileContent = parseHexclaveConfigFileContent;
|
|
150
|
-
exports.renderConfigFileContent = renderConfigFileContent;
|
|
151
|
-
exports.showOnboardingHexclaveConfigValue = showOnboardingHexclaveConfigValue;
|
|
152
|
-
exports.tryParseHexclaveConfigFileContent = tryParseHexclaveConfigFileContent;
|
|
153
|
-
//# sourceMappingURL=hexclave-config-file.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hexclave-config-file.js","names":["t","parser"],"sources":["../src/hexclave-config-file.ts"],"sourcesContent":["import * as parser from \"@babel/parser\";\nimport * as t from \"@babel/types\";\nimport { isValidConfig, normalize } from \"./config/format\";\n\nexport const showOnboardingHexclaveConfigValue = \"show-onboarding\";\n\nconst DEFAULT_CONFIG_IMPORT_PACKAGE = \"@hexclave/js\";\n\n/**\n * Renders a config object into the source text of a `stack.config.ts` file.\n *\n * Browser-safe: kept here (next to `parseHexclaveConfigFileContent`) instead of in\n * `config-rendering.ts` so dashboard client code can render config files\n * without pulling in `fs` / `path`.\n */\nexport function renderConfigFileContent(config: unknown, importPackage?: string): string {\n if (!isValidConfig(config)) {\n throw new Error(\"Invalid config: expected a plain object.\");\n }\n\n const droppedKeys: string[] = [];\n const normalizedConfig = normalize(config, {\n onDotIntoNonObject: \"ignore\",\n onDotIntoNull: \"empty-object\",\n droppedKeys,\n });\n if (droppedKeys.length > 0) {\n throw new Error(`Config has conflicting keys that would be dropped during normalization: ${droppedKeys.map(k => JSON.stringify(k)).join(\", \")}`);\n }\n const pkg = importPackage ?? DEFAULT_CONFIG_IMPORT_PACKAGE;\n // Import the `HexclaveConfig` type from the package's lightweight `/config`\n // entrypoint, which is free of framework runtime code and therefore safe for\n // tooling (e.g. the local dashboard) to load in a plain Node context. Only the\n // Hexclave-branded packages expose this subpath; legacy `@stackframe/*`\n // releases predate it, so fall back to their package root.\n const importSpecifier = pkg.startsWith(\"@hexclave/\") ? `${pkg}/config` : pkg;\n const importLine = `import type { HexclaveConfig } from \"${importSpecifier}\";`;\n return `${importLine}\\n\\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\\n`;\n}\n\ntype ParsedStackConfig = Record<string, unknown> | typeof showOnboardingHexclaveConfigValue;\n\nfunction unwrapStaticConfigExpression(expression: t.Expression): t.Expression {\n if (\n t.isTSAsExpression(expression)\n || t.isTSSatisfiesExpression(expression)\n || t.isTSTypeAssertion(expression)\n || t.isTSNonNullExpression(expression)\n ) {\n return unwrapStaticConfigExpression(expression.expression);\n }\n return expression;\n}\n\nfunction evaluateStaticConfigExpression(expression: t.Expression): unknown {\n const unwrapped = unwrapStaticConfigExpression(expression);\n if (t.isStringLiteral(unwrapped)) return unwrapped.value;\n if (t.isBooleanLiteral(unwrapped)) return unwrapped.value;\n if (t.isNumericLiteral(unwrapped)) return unwrapped.value;\n if (t.isNullLiteral(unwrapped)) return null;\n if (t.isIdentifier(unwrapped) && unwrapped.name === \"undefined\") return undefined;\n if (t.isUnaryExpression(unwrapped) && unwrapped.operator === \"-\" && t.isNumericLiteral(unwrapped.argument)) {\n return -unwrapped.argument.value;\n }\n if (t.isArrayExpression(unwrapped)) {\n return unwrapped.elements.map((element) => {\n if (element == null || t.isSpreadElement(element)) {\n throw new Error(\"Config arrays cannot contain holes or spreads.\");\n }\n return evaluateStaticConfigExpression(element);\n });\n }\n if (t.isObjectExpression(unwrapped)) {\n const result: Record<string, unknown> = {};\n for (const property of unwrapped.properties) {\n if (t.isSpreadElement(property)) {\n throw new Error(\"Config objects cannot contain spreads.\");\n }\n if (property.computed) {\n throw new Error(\"Config object keys cannot be computed.\");\n }\n const key = t.isIdentifier(property.key)\n ? property.key.name\n : t.isStringLiteral(property.key) || t.isNumericLiteral(property.key)\n ? String(property.key.value)\n : null;\n if (key == null) {\n throw new Error(\"Unsupported config object key.\");\n }\n if (t.isObjectMethod(property)) {\n throw new Error(\"Config objects cannot contain methods.\");\n }\n if (!t.isExpression(property.value)) {\n throw new Error(\"Unsupported config object value.\");\n }\n result[key] = evaluateStaticConfigExpression(property.value);\n }\n return result;\n }\n throw new Error(`Unsupported config expression: ${unwrapped.type}`);\n}\n\n/**\n * Like {@link parseHexclaveConfigFileContent}, but returns `null` instead of\n * throwing when the file is not a plain static config (e.g. it wraps the config\n * in a helper call, references imported values, or has a syntax error). Useful\n * for deciding whether a config file can be safely regenerated deterministically\n * or whether it has custom structure that must be preserved.\n */\nexport function tryParseHexclaveConfigFileContent(content: string, filePath: string): ParsedStackConfig | null {\n try {\n return parseHexclaveConfigFileContent(content, filePath);\n } catch {\n return null;\n }\n}\n\n/**\n * Returns whether `content` parses as a module that exports a `config` binding.\n * Used as a lightweight structural sanity check after editing config files whose\n * values can't be evaluated by our loader (e.g. they import external text\n * files), where a full semantic comparison isn't possible.\n */\nexport function hexclaveConfigFileExportsConfig(content: string, filePath: string): boolean {\n let ast: parser.ParseResult<t.File>;\n try {\n ast = parser.parse(content, {\n sourceType: \"module\",\n sourceFilename: filePath,\n plugins: [\"typescript\", \"importAttributes\"],\n });\n } catch {\n return false;\n }\n for (const statement of ast.program.body) {\n if (!t.isExportNamedDeclaration(statement)) {\n continue;\n }\n // Ignore type-only exports (`export type { config }`): they don't produce a\n // runtime `config` value, so they must not satisfy the structural check.\n if (statement.exportKind === \"type\") {\n continue;\n }\n if (t.isVariableDeclaration(statement.declaration)) {\n for (const declaration of statement.declaration.declarations) {\n if (t.isIdentifier(declaration.id) && declaration.id.name === \"config\" && declaration.init != null) {\n return true;\n }\n }\n }\n for (const specifier of statement.specifiers) {\n if (t.isExportSpecifier(specifier) && specifier.exportKind !== \"type\") {\n const exportedName = t.isIdentifier(specifier.exported) ? specifier.exported.name : specifier.exported.value;\n if (exportedName === \"config\") {\n return true;\n }\n }\n }\n }\n return false;\n}\n\n/**\n * Returns the relative import sources (those starting with `./` or `../`)\n * declared in `content`. Used to discover the external files a config update may\n * touch — e.g. `import x from \"./welcome-email.tsx\" with { type: \"text\" }` — so\n * they can be snapshotted and rolled back if an in-place update fails. Returns\n * an empty array if the file can't be parsed.\n */\nexport function getRelativeImportSpecifiers(content: string): string[] {\n let ast: parser.ParseResult<t.File>;\n try {\n ast = parser.parse(content, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"importAttributes\"],\n });\n } catch {\n return [];\n }\n const sources: string[] = [];\n for (const statement of ast.program.body) {\n if (t.isImportDeclaration(statement)) {\n const source = statement.source.value;\n if (source.startsWith(\"./\") || source.startsWith(\"../\")) {\n sources.push(source);\n }\n }\n }\n return sources;\n}\n\nexport function parseHexclaveConfigFileContent(content: string, filePath: string): ParsedStackConfig {\n if (content.trim() === \"\") return {};\n const ast = parser.parse(content, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"importAttributes\"],\n });\n\n for (const statement of ast.program.body) {\n if (!t.isExportNamedDeclaration(statement) || !t.isVariableDeclaration(statement.declaration)) {\n continue;\n }\n for (const declaration of statement.declaration.declarations) {\n if (!t.isIdentifier(declaration.id) || declaration.id.name !== \"config\") {\n continue;\n }\n if (declaration.init == null || !t.isExpression(declaration.init)) {\n throw new Error(`Config export in ${filePath} must have an initializer.`);\n }\n return evaluateStaticConfigExpression(declaration.init) as ParsedStackConfig;\n }\n }\n\n throw new Error(`Invalid config in ${filePath}. The file must export a plain \\`config\\` object or \"show-onboarding\".`);\n}\n"],"mappings":"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"}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import * as parser from "@babel/parser";
|
|
2
|
-
import * as t from "@babel/types";
|
|
3
|
-
import { isValidConfig, normalize } from "./config/format";
|
|
4
|
-
|
|
5
|
-
export const showOnboardingHexclaveConfigValue = "show-onboarding";
|
|
6
|
-
|
|
7
|
-
const DEFAULT_CONFIG_IMPORT_PACKAGE = "@hexclave/js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Renders a config object into the source text of a `stack.config.ts` file.
|
|
11
|
-
*
|
|
12
|
-
* Browser-safe: kept here (next to `parseHexclaveConfigFileContent`) instead of in
|
|
13
|
-
* `config-rendering.ts` so dashboard client code can render config files
|
|
14
|
-
* without pulling in `fs` / `path`.
|
|
15
|
-
*/
|
|
16
|
-
export function renderConfigFileContent(config: unknown, importPackage?: string): string {
|
|
17
|
-
if (!isValidConfig(config)) {
|
|
18
|
-
throw new Error("Invalid config: expected a plain object.");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const droppedKeys: string[] = [];
|
|
22
|
-
const normalizedConfig = normalize(config, {
|
|
23
|
-
onDotIntoNonObject: "ignore",
|
|
24
|
-
onDotIntoNull: "empty-object",
|
|
25
|
-
droppedKeys,
|
|
26
|
-
});
|
|
27
|
-
if (droppedKeys.length > 0) {
|
|
28
|
-
throw new Error(`Config has conflicting keys that would be dropped during normalization: ${droppedKeys.map(k => JSON.stringify(k)).join(", ")}`);
|
|
29
|
-
}
|
|
30
|
-
const pkg = importPackage ?? DEFAULT_CONFIG_IMPORT_PACKAGE;
|
|
31
|
-
// Import the `HexclaveConfig` type from the package's lightweight `/config`
|
|
32
|
-
// entrypoint, which is free of framework runtime code and therefore safe for
|
|
33
|
-
// tooling (e.g. the local dashboard) to load in a plain Node context. Only the
|
|
34
|
-
// Hexclave-branded packages expose this subpath; legacy `@stackframe/*`
|
|
35
|
-
// releases predate it, so fall back to their package root.
|
|
36
|
-
const importSpecifier = pkg.startsWith("@hexclave/") ? `${pkg}/config` : pkg;
|
|
37
|
-
const importLine = `import type { HexclaveConfig } from "${importSpecifier}";`;
|
|
38
|
-
return `${importLine}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
type ParsedStackConfig = Record<string, unknown> | typeof showOnboardingHexclaveConfigValue;
|
|
42
|
-
|
|
43
|
-
function unwrapStaticConfigExpression(expression: t.Expression): t.Expression {
|
|
44
|
-
if (
|
|
45
|
-
t.isTSAsExpression(expression)
|
|
46
|
-
|| t.isTSSatisfiesExpression(expression)
|
|
47
|
-
|| t.isTSTypeAssertion(expression)
|
|
48
|
-
|| t.isTSNonNullExpression(expression)
|
|
49
|
-
) {
|
|
50
|
-
return unwrapStaticConfigExpression(expression.expression);
|
|
51
|
-
}
|
|
52
|
-
return expression;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function evaluateStaticConfigExpression(expression: t.Expression): unknown {
|
|
56
|
-
const unwrapped = unwrapStaticConfigExpression(expression);
|
|
57
|
-
if (t.isStringLiteral(unwrapped)) return unwrapped.value;
|
|
58
|
-
if (t.isBooleanLiteral(unwrapped)) return unwrapped.value;
|
|
59
|
-
if (t.isNumericLiteral(unwrapped)) return unwrapped.value;
|
|
60
|
-
if (t.isNullLiteral(unwrapped)) return null;
|
|
61
|
-
if (t.isIdentifier(unwrapped) && unwrapped.name === "undefined") return undefined;
|
|
62
|
-
if (t.isUnaryExpression(unwrapped) && unwrapped.operator === "-" && t.isNumericLiteral(unwrapped.argument)) {
|
|
63
|
-
return -unwrapped.argument.value;
|
|
64
|
-
}
|
|
65
|
-
if (t.isArrayExpression(unwrapped)) {
|
|
66
|
-
return unwrapped.elements.map((element) => {
|
|
67
|
-
if (element == null || t.isSpreadElement(element)) {
|
|
68
|
-
throw new Error("Config arrays cannot contain holes or spreads.");
|
|
69
|
-
}
|
|
70
|
-
return evaluateStaticConfigExpression(element);
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
if (t.isObjectExpression(unwrapped)) {
|
|
74
|
-
const result: Record<string, unknown> = {};
|
|
75
|
-
for (const property of unwrapped.properties) {
|
|
76
|
-
if (t.isSpreadElement(property)) {
|
|
77
|
-
throw new Error("Config objects cannot contain spreads.");
|
|
78
|
-
}
|
|
79
|
-
if (property.computed) {
|
|
80
|
-
throw new Error("Config object keys cannot be computed.");
|
|
81
|
-
}
|
|
82
|
-
const key = t.isIdentifier(property.key)
|
|
83
|
-
? property.key.name
|
|
84
|
-
: t.isStringLiteral(property.key) || t.isNumericLiteral(property.key)
|
|
85
|
-
? String(property.key.value)
|
|
86
|
-
: null;
|
|
87
|
-
if (key == null) {
|
|
88
|
-
throw new Error("Unsupported config object key.");
|
|
89
|
-
}
|
|
90
|
-
if (t.isObjectMethod(property)) {
|
|
91
|
-
throw new Error("Config objects cannot contain methods.");
|
|
92
|
-
}
|
|
93
|
-
if (!t.isExpression(property.value)) {
|
|
94
|
-
throw new Error("Unsupported config object value.");
|
|
95
|
-
}
|
|
96
|
-
result[key] = evaluateStaticConfigExpression(property.value);
|
|
97
|
-
}
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
throw new Error(`Unsupported config expression: ${unwrapped.type}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Like {@link parseHexclaveConfigFileContent}, but returns `null` instead of
|
|
105
|
-
* throwing when the file is not a plain static config (e.g. it wraps the config
|
|
106
|
-
* in a helper call, references imported values, or has a syntax error). Useful
|
|
107
|
-
* for deciding whether a config file can be safely regenerated deterministically
|
|
108
|
-
* or whether it has custom structure that must be preserved.
|
|
109
|
-
*/
|
|
110
|
-
export function tryParseHexclaveConfigFileContent(content: string, filePath: string): ParsedStackConfig | null {
|
|
111
|
-
try {
|
|
112
|
-
return parseHexclaveConfigFileContent(content, filePath);
|
|
113
|
-
} catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Returns whether `content` parses as a module that exports a `config` binding.
|
|
120
|
-
* Used as a lightweight structural sanity check after editing config files whose
|
|
121
|
-
* values can't be evaluated by our loader (e.g. they import external text
|
|
122
|
-
* files), where a full semantic comparison isn't possible.
|
|
123
|
-
*/
|
|
124
|
-
export function hexclaveConfigFileExportsConfig(content: string, filePath: string): boolean {
|
|
125
|
-
let ast: parser.ParseResult<t.File>;
|
|
126
|
-
try {
|
|
127
|
-
ast = parser.parse(content, {
|
|
128
|
-
sourceType: "module",
|
|
129
|
-
sourceFilename: filePath,
|
|
130
|
-
plugins: ["typescript", "importAttributes"],
|
|
131
|
-
});
|
|
132
|
-
} catch {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
for (const statement of ast.program.body) {
|
|
136
|
-
if (!t.isExportNamedDeclaration(statement)) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
// Ignore type-only exports (`export type { config }`): they don't produce a
|
|
140
|
-
// runtime `config` value, so they must not satisfy the structural check.
|
|
141
|
-
if (statement.exportKind === "type") {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (t.isVariableDeclaration(statement.declaration)) {
|
|
145
|
-
for (const declaration of statement.declaration.declarations) {
|
|
146
|
-
if (t.isIdentifier(declaration.id) && declaration.id.name === "config" && declaration.init != null) {
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
for (const specifier of statement.specifiers) {
|
|
152
|
-
if (t.isExportSpecifier(specifier) && specifier.exportKind !== "type") {
|
|
153
|
-
const exportedName = t.isIdentifier(specifier.exported) ? specifier.exported.name : specifier.exported.value;
|
|
154
|
-
if (exportedName === "config") {
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Returns the relative import sources (those starting with `./` or `../`)
|
|
165
|
-
* declared in `content`. Used to discover the external files a config update may
|
|
166
|
-
* touch — e.g. `import x from "./welcome-email.tsx" with { type: "text" }` — so
|
|
167
|
-
* they can be snapshotted and rolled back if an in-place update fails. Returns
|
|
168
|
-
* an empty array if the file can't be parsed.
|
|
169
|
-
*/
|
|
170
|
-
export function getRelativeImportSpecifiers(content: string): string[] {
|
|
171
|
-
let ast: parser.ParseResult<t.File>;
|
|
172
|
-
try {
|
|
173
|
-
ast = parser.parse(content, {
|
|
174
|
-
sourceType: "module",
|
|
175
|
-
plugins: ["typescript", "importAttributes"],
|
|
176
|
-
});
|
|
177
|
-
} catch {
|
|
178
|
-
return [];
|
|
179
|
-
}
|
|
180
|
-
const sources: string[] = [];
|
|
181
|
-
for (const statement of ast.program.body) {
|
|
182
|
-
if (t.isImportDeclaration(statement)) {
|
|
183
|
-
const source = statement.source.value;
|
|
184
|
-
if (source.startsWith("./") || source.startsWith("../")) {
|
|
185
|
-
sources.push(source);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return sources;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function parseHexclaveConfigFileContent(content: string, filePath: string): ParsedStackConfig {
|
|
193
|
-
if (content.trim() === "") return {};
|
|
194
|
-
const ast = parser.parse(content, {
|
|
195
|
-
sourceType: "module",
|
|
196
|
-
plugins: ["typescript", "importAttributes"],
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
for (const statement of ast.program.body) {
|
|
200
|
-
if (!t.isExportNamedDeclaration(statement) || !t.isVariableDeclaration(statement.declaration)) {
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
for (const declaration of statement.declaration.declarations) {
|
|
204
|
-
if (!t.isIdentifier(declaration.id) || declaration.id.name !== "config") {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (declaration.init == null || !t.isExpression(declaration.init)) {
|
|
208
|
-
throw new Error(`Config export in ${filePath} must have an initializer.`);
|
|
209
|
-
}
|
|
210
|
-
return evaluateStaticConfigExpression(declaration.init) as ParsedStackConfig;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
throw new Error(`Invalid config in ${filePath}. The file must export a plain \`config\` object or "show-onboarding".`);
|
|
215
|
-
}
|