fix(desktop): make the desktop typecheck chain fully green

The electron tsconfig, renderer, e2e and electron-builder checkJs passes
now all pass cleanly. Three pre-existing issues fixed (the branch is
ahead of upstream/main, where none of these files exist):

- package-process-reap.ts: guard `isUnderInstallRoot` loop members with
  `typeof root !== 'string'` so the TS union (string | readonly string[])
  narrows before normalize() — the old `if (!root)` did not narrow arrays.
- msix-shared.mjs: add JSDoc param/return types to the four git-tag
  helpers and the stamp/XML/Content-Type helpers so the electron-builder
  checkJs pass (which walks this module via its import graph) stops
  flagging implicit-any.
- electron-builder.config.cjs: bind storeMsix to a local + a
  mustStoreMsix() assertion helper — `store` is typed boolean, so checkJs
  cannot correlate it with the optional storeMsix; the helper is the
  single documented assertion point (checkJs forbids `!`).
This commit is contained in:
ethernet
2026-09-01 22:53:18 -04:00
parent 1560aad4ae
commit 6c154fcb8b
3 changed files with 62 additions and 5 deletions

View File

@@ -24,6 +24,24 @@ const {
msixAppIdWithOrg
} = require('./product-identity.cjs')
// `storeMsix` is optional on the identity type but guaranteed present when
// `store` is true (product-identity.cjs spreads it only in that branch).
// The `store` flag is typed `boolean` in the identity, so checkJs cannot
// correlate the two; `mustStoreMsix` is the single assertion point and the
// invariant lives in product-identity.cjs:33-34/58-68.
/** @type {NonNullable<typeof storeMsix> | undefined} */
const storeMsixWhenStore = storeMsix
/**
* The store MSIX packaging identity. Callers must only invoke this when
* `store` is true (see the invariant note above).
* @param {NonNullable<typeof storeMsix> | undefined} value
* @returns {NonNullable<typeof storeMsix>}
*/
function mustStoreMsix(value) {
return /** @type {NonNullable<typeof storeMsix>} */ (value)
}
// The out-of-store MSIX publisher (ATS cert subject) — single source, shared
// with the .appinstaller generator so the manifest and the App Installer can
// never drift (see scripts/msix-shared.mjs).
@@ -166,11 +184,11 @@ module.exports = {
// A store build uses the Partner Center packaging identity (the Store
// re-signs + rewrites the publisher on submission); everything else uses
// the out-of-store ATS-cert identity.
identityName: store ? storeMsix.identityName : msixAppIdWithOrg,
identityName: store ? mustStoreMsix(storeMsixWhenStore).identityName : msixAppIdWithOrg,
applicationId: appNamePascal,
displayName,
publisher: store ? storeMsix.publisher : OUT_OF_STORE_PUBLISHER,
publisherDisplayName: store ? storeMsix.publisherDisplayName : 'Nous Research',
publisher: store ? mustStoreMsix(storeMsixWhenStore).publisher : OUT_OF_STORE_PUBLISHER,
publisherDisplayName: store ? mustStoreMsix(storeMsixWhenStore).publisherDisplayName : 'Nous Research',
// Canary MSIX versions are `X.Y.Z.<minutes-since-stable>` (see
// scripts/msix-shared.mjs). setBuildNumber makes getVersionInWeirdWindowsForm
// use the BUILD_NUMBER env (4th component) instead of hardcoding ".0" — a

View File

@@ -198,7 +198,7 @@ export function isUnderInstallRoot(
const candidates = typeof roots === 'string' || roots == null ? [roots] : roots
for (const root of candidates) {
if (!root) {
if (typeof root !== 'string' || !root) {
continue
}

View File

@@ -31,7 +31,11 @@ const CONTENT_TYPES = {
'.msix': 'application/msix'
}
/** The Content-Type to store for a staged release artifact, if any. */
/**
* The Content-Type to store for a staged release artifact, if any.
* @param {string} filename the artifact filename
* @returns {string | undefined}
*/
export function contentTypeFor(filename) {
const lower = String(filename).toLowerCase()
for (const [suffix, mime] of Object.entries(CONTENT_TYPES)) {
@@ -110,6 +114,10 @@ export function resolveWinSdkTools() {
process.exit(1)
}
/**
* @param {unknown} value any value to XML-escape
* @returns {string}
*/
function escapeAttr(value) {
return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
}
@@ -163,6 +171,10 @@ const STABLE_TAG_RE = /^v\d+\.\d+\.\d+$/
// number to clamp (a clamped number would break monotonicity).
const MAX_BUILD_MINUTES = 45 * 24 * 60
/**
* @param {string} stamp YYYYMMDD[HHMMSS] UTC stamp
* @returns {number} epoch seconds
*/
function stampToEpoch(stamp) {
const parts = /^(\d{4})(\d{2})(\d{2})(\d{2})?(\d{2})?(\d{2})?$/.exec(stamp)
if (!parts) return 0
@@ -170,15 +182,35 @@ function stampToEpoch(stamp) {
return Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h ?? 0), Number(mi ?? 0), Number(s ?? 0)) / 1000
}
/**
* List git tags matching `pattern`, newest-first (git's -v:refname sort).
* @param {string} gitRoot the repo root
* @param {string} pattern git tag glob, e.g. "v0.27.*"
* @returns {string[]}
*/
export function listGitTags(gitRoot, pattern) {
return execFileSync('git', ['tag', '--list', pattern, '--sort=-v:refname'], { cwd: gitRoot, encoding: 'utf8' })
.split('\n').filter(Boolean)
}
/**
* The commit time (epoch seconds) of `tag`, for minutes-since-stable math.
* @param {string} gitRoot the repo root
* @param {string} tag a git tag
* @returns {number}
*/
export function gitTagCommitTime(gitRoot, tag) {
return Number(execFileSync('git', ['log', '-1', '--format=%ct', tag], { cwd: gitRoot, encoding: 'utf8' }).trim())
}
/**
* Minutes between a canary tag's UTC stamp and the given stable epoch —
* the MSIX 4th version component. Null for a stable tag; throws when the
* stable base is older than 45 days (16-bit component would overflow).
* @param {string} tag the release tag
* @param {number} stableEpoch stable tag commit time, epoch seconds
* @returns {number | null}
*/
export function canaryBuildMinutesFor(tag, stableEpoch) {
const m = CANARY_TAG_RE.exec(String(tag || ''))
if (!m) return null
@@ -193,6 +225,13 @@ export function canaryBuildMinutesFor(tag, stableEpoch) {
return minutes
}
/**
* Minutes-since-stable for a canary tag, resolving the stable base from
* the repo's tags on the same major.minor line.
* @param {string} tag the release tag
* @param {string} gitRoot the repo root
* @returns {number | null}
*/
export function canaryBuildMinutes(tag, gitRoot) {
const m = CANARY_TAG_RE.exec(String(tag || ''))
if (!m) return null