Skip to content

Commit b6a61b5

Browse files
author
bgagent
committed
fix(scripts): fix failing precommit
1 parent 5979765 commit b6a61b5

1 file changed

Lines changed: 90 additions & 4 deletions

File tree

scripts/check-types-sync.ts

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ interface ExportSummary {
6060
const REPO_ROOT = path.resolve(import.meta.dirname, '..');
6161
const CDK_TYPES_FILE = path.join(REPO_ROOT, 'cdk/src/handlers/shared/types.ts');
6262
const CLI_TYPES_FILE = path.join(REPO_ROOT, 'cli/src/types.ts');
63+
const CONSTANTS_JSON_FILE = path.join(REPO_ROOT, 'contracts/constants.json');
64+
65+
/**
66+
* ``contracts/constants.json`` is the single source of truth for the
67+
* shared numeric bounds (``check-constants-sync.ts`` enforces that
68+
* Python and the JSON agree). The CDK imports these directly
69+
* (``import sharedConstants from '.../constants.json'``) because its
70+
* handlers are esbuild-bundled at deploy; the CLI is a plain
71+
* ``tsc --build`` published package whose ``rootDir`` can't reach the
72+
* file, so it mirrors the same numbers as literals. To compare the two
73+
* fairly we resolve any ``<alias>.a.b`` reference to a value loaded from
74+
* the JSON, so ``sharedConstants.approval_timeout_s.min`` and ``30``
75+
* normalize to the same shape — while a genuine literal drift (``60``)
76+
* still trips the diff.
77+
*/
78+
const SHARED_CONSTANTS: Record<string, unknown> = JSON.parse(
79+
fs.readFileSync(CONSTANTS_JSON_FILE, 'utf-8'),
80+
);
6381

6482
/**
6583
* Names that are intentionally CDK-only — handler-internal shapes
@@ -146,6 +164,13 @@ function parseFile(filePath: string): Map<string, ExportSummary> {
146164
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
147165
const exports = new Map<string, ExportSummary>();
148166

167+
// Local names bound to a default-import of contracts/constants.json
168+
// (e.g. ``import sharedConstants from '.../constants.json'``). Used to
169+
// resolve ``sharedConstants.approval_timeout_s.min`` to its JSON value
170+
// when summarizing literal constants, so a JSON-sourced reference and a
171+
// mirrored literal compare equal.
172+
const constantsAliases = collectConstantsAliases(sourceFile);
173+
149174
for (const node of sourceFile.statements) {
150175
// Re-export declarations (``export type { Foo } from './bar'`` or
151176
// ``export type { Foo };`` after an import) are top-level
@@ -169,7 +194,7 @@ function parseFile(filePath: string): Map<string, ExportSummary> {
169194
// Constants like `export const APPROVAL_TIMEOUT_S_MIN = 30`.
170195
for (const decl of node.declarationList.declarations) {
171196
if (ts.isIdentifier(decl.name)) {
172-
exports.set(decl.name.text, summarizeLiteralConst(decl));
197+
exports.set(decl.name.text, summarizeLiteralConst(decl, constantsAliases));
173198
}
174199
}
175200
} else if (ts.isFunctionDeclaration(node) && node.name) {
@@ -215,10 +240,71 @@ function summarizeTypeAlias(node: ts.TypeAliasDeclaration): ExportSummary {
215240
return { kind: 'type-alias', shape: sorted };
216241
}
217242

218-
function summarizeLiteralConst(decl: ts.VariableDeclaration): ExportSummary {
243+
/**
244+
* Collect local identifiers bound to a default-import of
245+
* ``contracts/constants.json`` so property accesses through them can be
246+
* resolved to concrete JSON values. Matches the import by specifier
247+
* basename (``constants.json``) regardless of the relative path depth,
248+
* which differs between the CDK (``../../../../``) and any future CLI use.
249+
*/
250+
function collectConstantsAliases(sourceFile: ts.SourceFile): Set<string> {
251+
const aliases = new Set<string>();
252+
for (const node of sourceFile.statements) {
253+
if (
254+
ts.isImportDeclaration(node) &&
255+
ts.isStringLiteral(node.moduleSpecifier) &&
256+
path.basename(node.moduleSpecifier.text) === 'constants.json' &&
257+
node.importClause?.name
258+
) {
259+
aliases.add(node.importClause.name.text);
260+
}
261+
}
262+
return aliases;
263+
}
264+
265+
/**
266+
* Resolve a ``<alias>.a.b`` property-access chain rooted at a
267+
* constants.json default-import to its JSON value, returning the value's
268+
* textual form (e.g. ``30``). Returns ``undefined`` for any expression
269+
* that isn't such a chain or doesn't resolve to a primitive in the JSON.
270+
*/
271+
function resolveConstantsReference(
272+
expr: ts.Expression,
273+
aliases: Set<string>,
274+
): string | undefined {
275+
if (aliases.size === 0 || !ts.isPropertyAccessExpression(expr)) return undefined;
276+
const segments: string[] = [];
277+
let cursor: ts.Expression = expr;
278+
while (ts.isPropertyAccessExpression(cursor)) {
279+
segments.unshift(cursor.name.text);
280+
cursor = cursor.expression;
281+
}
282+
if (!ts.isIdentifier(cursor) || !aliases.has(cursor.text)) return undefined;
283+
let value: unknown = SHARED_CONSTANTS;
284+
for (const seg of segments) {
285+
if (value == null || typeof value !== 'object') return undefined;
286+
value = (value as Record<string, unknown>)[seg];
287+
}
288+
if (value == null || typeof value === 'object') return undefined;
289+
return String(value);
290+
}
291+
292+
function summarizeLiteralConst(
293+
decl: ts.VariableDeclaration,
294+
constantsAliases: Set<string>,
295+
): ExportSummary {
219296
// Capture the textual initializer so a value drift (e.g.
220-
// APPROVAL_TIMEOUT_S_MIN = 30 vs 60) gets flagged. Whitespace
221-
// normalized to keep formatting churn out of the diff.
297+
// APPROVAL_TIMEOUT_S_MIN = 30 vs 60) gets flagged. A reference into
298+
// contracts/constants.json (e.g. ``sharedConstants.approval_timeout_s.min``)
299+
// is first resolved to its JSON value so it compares equal to the
300+
// mirrored literal on the other side. Whitespace normalized to keep
301+
// formatting churn out of the diff.
302+
if (decl.initializer) {
303+
const resolved = resolveConstantsReference(decl.initializer, constantsAliases);
304+
if (resolved !== undefined) {
305+
return { kind: 'literal-const', shape: resolved };
306+
}
307+
}
222308
const init = decl.initializer ? decl.initializer.getText().replace(/\s+/g, ' ').trim() : '';
223309
return { kind: 'literal-const', shape: init };
224310
}

0 commit comments

Comments
 (0)