Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 39 additions & 1 deletion cli/src/util/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> = 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<unknown> => {
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) => {
Expand Down