From 2841bcfa09c82be0892c2d3a54c8e09872be7695 Mon Sep 17 00:00:00 2001 From: conor Date: Mon, 20 Jul 2026 10:51:40 -0700 Subject: [PATCH] fix(cli): support TypeScript 7 when loading capacitor.config.ts TypeScript 7 dropped the classic compiler API (transpileModule, ModuleKind, etc.) from its default export, so requireTS's use of ts.transpileModule throws on projects with typescript@7 installed and every command that reads capacitor.config.ts fails immediately. Detect when that API isn't available and fall back to Node's own built-in TypeScript syntax stripping via a real dynamic import (stable since Node 23.6, available behind --experimental-strip-types since Node 22.6) instead of transpiling the file ourselves. Falls back to a clear error if neither is available. Projects on older TypeScript versions are unaffected: the existing transpileModule path is unchanged. Fixes #8531 --- cli/src/config.ts | 2 +- cli/src/util/node.ts | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/cli/src/config.ts b/cli/src/config.ts index 4e71fb01d1..bb71177dcd 100644 --- a/cli/src/config.ts +++ b/cli/src/config.ts @@ -113,7 +113,7 @@ async function loadExtConfigTS( } const ts = require(tsPath); // eslint-disable-line @typescript-eslint/no-var-requires - const extConfigObject = requireTS(ts, extConfigFilePath) as any; + const extConfigObject = (await requireTS(ts, extConfigFilePath)) as any; const extConfig = extConfigObject.default ? await extConfigObject.default : extConfigObject; return { diff --git a/cli/src/util/node.ts b/cli/src/util/node.ts index 299dffdd3f..6f511da25b 100644 --- a/cli/src/util/node.ts +++ b/cli/src/util/node.ts @@ -2,17 +2,55 @@ import { existsSync } from 'fs'; import { readFileSync } from 'fs-extra'; import { resolve } from 'path'; import type typescript from 'typescript'; +import { pathToFileURL } from 'url'; interface NodeModuleWithCompile extends NodeJS.Module { _compile?(code: string, filename: string): any; } +/** + * TypeScript 7 dropped the classic compiler API (transpileModule, ModuleKind, etc.) from its + * default export -- `require('typescript')` now only exposes `version`/`versionMajorMinor`. + * Detect that case so we can fall back to a loading strategy that doesn't depend on it. + * + * @see https://github.com/ionic-team/capacitor/issues/8531 + */ +function hasClassicCompilerAPI(ts: typeof typescript): boolean { + return typeof ts.transpileModule === 'function'; +} + +/** + * A plain `import()` here gets downleveled to a `require()` call by tsc when compiling to + * CommonJS (our build target), which defeats the purpose -- we specifically need the real ESM + * loader so Node's native TypeScript stripping kicks in. Constructing the import from a string + * keeps tsc from touching it. + */ +const dynamicImport: (specifier: string) => Promise = new Function('specifier', 'return import(specifier)') as any; + /** * @see https://github.com/ionic-team/stencil/blob/HEAD/src/compiler/sys/node-require.ts */ -export const requireTS = (ts: typeof typescript, p: string): unknown => { +export const requireTS = async (ts: typeof typescript, p: string): Promise => { const id = resolve(p); + if (!hasClassicCompilerAPI(ts)) { + // Node has its own built-in TypeScript syntax stripping (stable since Node 23.6, and + // available behind --experimental-strip-types since Node 22.6), so we can load the file + // directly via the native ESM loader instead of transpiling it ourselves. + try { + return await dynamicImport(pathToFileURL(id).href); + } catch (e: any) { + if (e?.code === 'ERR_UNKNOWN_FILE_EXTENSION') { + throw new Error( + `Your installed version of TypeScript (${ts.version}) no longer provides the compiler API Capacitor previously used to load .ts config files, ` + + `and your Node.js runtime (${process.version}) doesn't support loading them natively either.\n` + + 'Upgrade to Node.js 22.6+ (running with --experimental-strip-types), or Node.js 23.6+, to continue using capacitor.config.ts.', + ); + } + throw e; + } + } + delete require.cache[id]; require.extensions['.ts'] = (module: NodeModuleWithCompile, fileName: string) => {