From a04114f10776815b88c227707af0988c5ae8a1a5 Mon Sep 17 00:00:00 2001 From: Pieter Develtere Date: Thu, 9 Jul 2026 12:29:02 +0200 Subject: [PATCH] fix(register): pass imports with import attributes through to the default loader The resolve hook already defers to nextResolve when import attributes are present, but the load hook still compiled the resolved source as TypeScript. This breaks text imports (`with { type: 'text' }`, Node.js 26.5+ behind --experimental-import-text): the raw text was fed to swc and failed to parse. Skip the load hook when import attributes are present so the default loader handles the module, mirroring the existing resolve behaviour. --- packages/integrate-module/src/index.ts | 20 +++++++++++++++++++ .../integrate-module/src/text-import/data.txt | 1 + .../integrate-module/src/text-import/index.ts | 5 +++++ .../integrate-module/src/text-import/txt.d.ts | 4 ++++ packages/register/esm.mts | 7 +++++++ 5 files changed, 37 insertions(+) create mode 100644 packages/integrate-module/src/text-import/data.txt create mode 100644 packages/integrate-module/src/text-import/index.ts create mode 100644 packages/integrate-module/src/text-import/txt.d.ts diff --git a/packages/integrate-module/src/index.ts b/packages/integrate-module/src/index.ts index 4f4f78c62..730821d3c 100644 --- a/packages/integrate-module/src/index.ts +++ b/packages/integrate-module/src/index.ts @@ -1,6 +1,8 @@ /* eslint import/order: off */ import assert from 'node:assert' +import { spawnSync } from 'node:child_process' import test from 'node:test' +import { fileURLToPath } from 'node:url' import { RepositoryState } from '@napi-rs/simple-git' import { bar as subBar } from '@subdirectory/bar.mjs' @@ -102,3 +104,21 @@ await test('postgres should work', async () => { await test('resolve conditions', () => { assert.equal(name, 'from-dev') }) + +// `with { type: 'text' }` is supported since Node.js 26.5, behind --experimental-import-text +const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number) +const supportsTextImports = nodeMajor > 26 || (nodeMajor === 26 && nodeMinor >= 5) + +await test('text import attributes should pass through to the default loader', { skip: !supportsTextImports }, () => { + const { status, stderr } = spawnSync( + process.execPath, + [ + '--experimental-import-text', + '--import=@swc-node/register/esm-register', + fileURLToPath(new URL('./text-import/index.ts', import.meta.url)), + ], + { env: process.env }, + ) + + assert.equal(status, 0, stderr?.toString()) +}) diff --git a/packages/integrate-module/src/text-import/data.txt b/packages/integrate-module/src/text-import/data.txt new file mode 100644 index 000000000..6b4fa2c67 --- /dev/null +++ b/packages/integrate-module/src/text-import/data.txt @@ -0,0 +1 @@ +text import works diff --git a/packages/integrate-module/src/text-import/index.ts b/packages/integrate-module/src/text-import/index.ts new file mode 100644 index 000000000..0755cbb38 --- /dev/null +++ b/packages/integrate-module/src/text-import/index.ts @@ -0,0 +1,5 @@ +import assert from 'node:assert' + +import data from './data.txt' with { type: 'text' } + +assert.equal(data, 'text import works\n') diff --git a/packages/integrate-module/src/text-import/txt.d.ts b/packages/integrate-module/src/text-import/txt.d.ts new file mode 100644 index 000000000..6c041dbb8 --- /dev/null +++ b/packages/integrate-module/src/text-import/txt.d.ts @@ -0,0 +1,4 @@ +declare module '*.txt' { + const text: string + export default text +} diff --git a/packages/register/esm.mts b/packages/register/esm.mts index 9c1d6d91f..dd94a37d2 100644 --- a/packages/register/esm.mts +++ b/packages/register/esm.mts @@ -305,6 +305,13 @@ export const load: LoadHook = async (url, context, nextLoad) => { return nextLoad(url, context) } + // import attributes are handled by the default loader, + // e.g. `with { type: 'text' }` since Node.js 26.5 (behind --experimental-import-text) + if (context.importAttributes?.type) { + debug('skip load: import attributes', url) + return nextLoad(url, context) + } + const { source, format: resolvedFormat } = await nextLoad(url, context) if (!source) {