Share untyped module bindings that combine with strings and numbers (#2087)

This commit is contained in:
Chris Tate
2026-10-24 02:35:45 -05:00
committed by GitHub
9 changed files with 59 additions and 7 deletions
@@ -1183,19 +1183,22 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi
}
if (e.kind === ts.SyntaxKind.NullKeyword) return true;
if (ts.isIdentifier(e) && e.text === "undefined") return true;
if (ts.isConditionalExpression(e)) {
return (
jsDynHoldableInitializer(lowerer, e.whenTrue) &&
jsDynHoldableInitializer(lowerer, e.whenFalse)
);
}
// The arms of a conditional or logical expression: checked values, or
// strings, numbers and booleans, which box into the checked-dynamic
// result (`const APP_NAME = config.name || "pi"`).
const arm = (x: ts.Expression): boolean => {
if (jsDynHoldableInitializer(lowerer, x)) return true;
const kind = lowerer.mapTypeOf(lowerer.typeOf(x))?.kind;
return kind === "string" || kind === "f64" || kind === "bool";
};
if (ts.isConditionalExpression(e)) return arm(e.whenTrue) && arm(e.whenFalse);
if (
ts.isBinaryExpression(e) &&
(e.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
e.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
) {
return jsDynHoldableInitializer(lowerer, e.left) && jsDynHoldableInitializer(lowerer, e.right);
return arm(e.left) && arm(e.right);
}
// A member of a checked module value is itself checked, including a
// callable whose inferred overloads cannot describe one native ABI.
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8215-js-any-logical-module-bindings/main.ts"
],
"diags": []
}
@@ -0,0 +1,8 @@
// Module bindings of a JavaScript package whose initializers combine an
// untyped value with a string, number or boolean through `||`, `&&`, `??`
// or `?:` (pi-coding-agent's config.js): other modules and functions read
// the values as in Node.
import { banner, usage } from "app-config";
console.log(banner);
console.log(usage("check"));
@@ -0,0 +1,18 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
let pkg = {};
try {
pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
}
catch (e) {
if (e.code !== "ENOENT")
throw e;
}
const configName = pkg.appConfig?.name;
export const APP_NAME = configName || "app";
export const APP_TITLE = configName ? APP_NAME : "App";
export const CONFIG_DIR = pkg.appConfig?.configDir || ".config";
export const VERSION = pkg.version ?? "0.0.0";
export const RETRIES = pkg.appConfig?.retries ?? 3;
export const isBundled = typeof APP_BUNDLED !== "undefined" && APP_BUNDLED;
@@ -0,0 +1,2 @@
export declare const banner: string;
export declare function usage(kind: "check" | "list"): string;
@@ -0,0 +1 @@
export { banner, usage } from "./usage.js";
@@ -0,0 +1,12 @@
import { APP_NAME, APP_TITLE, CONFIG_DIR, RETRIES, VERSION, isBundled } from "./config.js";
const USAGE = {
check: `${APP_NAME} check --dir ${CONFIG_DIR}`,
list: `${APP_NAME} list`,
};
export const banner = `${APP_TITLE} ${VERSION} (bundled: ${isBundled}, retries: ${RETRIES})`;
export function usage(kind) {
return `${USAGE[kind]} [${APP_NAME.toUpperCase()}_HOME=${CONFIG_DIR}] ${typeof isBundled} ${RETRIES + 1}`;
}
@@ -0,0 +1 @@
{ "name": "app-config", "version": "2.1.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "appConfig": { "configDir": ".app" } }
@@ -0,0 +1 @@
{ "plain": 2500, "san": 3500 }