Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On Windows arm64 TS-based custom loader sometimes causes the process to exit silently when used with --experimental-strip-types #54645

Closed
Tracked by #17
wojtekmaj opened this issue Aug 29, 2024 · 39 comments
Labels
arm Issues and PRs related to the ARM platform. dependencies Pull requests that update a dependency file. strip-types Issues or PRs related to strip-types support windows Issues and PRs related to the Windows platform.

Comments

@wojtekmaj
Copy link

wojtekmaj commented Aug 29, 2024

Version

22.7.0

Platform

Microsoft Windows NT 10.0.26100.0 x64

Subsystem

No response

What steps will reproduce the bug?

I was trying to run my TypeScript + node16 resolution app using --experimental-strip-types.

To cope with --experimental-strip-types not working when using .js extension in import paths, I've decided to use --import, so this is the command I came up with:

node --import=./register.ts --experimental-strip-types ./test.js

register.ts

import { register } from 'node:module';

register('./hooks.ts', import.meta.url);

hooks.ts

import path from 'node:path';

import type { ResolveHook } from 'node:module';

const jsExts = new Set(['.js', '.jsx', '.mjs', '.cjs']);
const tsExts = new Set(['.ts', '.tsx', '.mts', '.cts']);

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
  const ext = path.extname(specifier);

  // If it's not a JS import, use default behavior
  if (!jsExts.has(ext)) {
    return nextResolve(specifier, context);
  }

  for (const tsExt of tsExts) {
    const specifierWithExtReplaced = specifier.replace(ext, tsExt);

    try {
      // Try and resolve file using each of TS extensions
      return await nextResolve(specifierWithExtReplaced, context);
    } catch {}
  }

  // If all else fails, fall back to default behavior
  return nextResolve(specifier, context);
};

test.ts

console.log('Hello, world!');

How often does it reproduce? Is there a required condition?

100% of the time, condition: Windows (I tested arm64, can't tell if that happens on other architectures as well).

What is the expected behavior? Why is that the expected behavior?

./test.ts to be executed

What do you see instead?

On macOS, this works flawlessly, executing ./test.ts file (mind the extension).

On Windows, however, this does not execute the file, and just silently exits after producing one ExperimentalWarning written with red text:

PS C:\server> node --import=./register.ts --experimental-strip-types ./test.js
(node:3112) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

Additional information

Note 1

If I rename register.ts and hooks.ts files to .js and strip types manually, it works:

PS C:\server> node --import=./register.js --experimental-strip-types ./test.js
(node:1648) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:1648) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Hello, world!

(side note: first ExperimentalWarning is in red, seconds one in white. I guess one would be enough? :D)

Note 2

In addition, the workaround I came up with (stripping types manually from register.ts and hooks.ts), does not work on a larger scale application for me. This, in turn, produces two ExperimentalWarning like above, but no code appears to be executed, so my guess is that resolution still fails at some point. When added logs, I observed that my resolution hook receives all specifiers it found in my app's entry file, but then stopped resolution altogether.

I logged that issue separately in #54665.

@avivkeller avivkeller added windows Issues and PRs related to the Windows platform. loaders Issues and PRs related to ES module loaders strip-types Issues or PRs related to strip-types support labels Aug 29, 2024
@avivkeller
Copy link
Member

Is this issue specific to the code you are importing, or is it imports (via --import) in general?

@wojtekmaj
Copy link
Author

wojtekmaj commented Aug 29, 2024

Looks like striping types from loaders issue, the one this issue is primarily about, is not specific to the code I'm importing. The example I provided is literally just a console.log, and it still fails.

The second issue that I experienced when using pure JS loaders instead needs further investigation so I'll report this separately. But so far, it seems it's stopping module resolution on the entry module, without any errors. I'd suspect it's my mistake, I'm no expert in Node.js loaders, but then why it works on macOS 👀

@avivkeller
Copy link
Member

CC @nodejs/loaders @nodejs/typescript

@aduh95
Copy link
Contributor

aduh95 commented Aug 30, 2024

Can you run it again with NODE_DEBUG set to * in your env?

@marco-ippolito
Copy link
Member

marco-ippolito commented Aug 30, 2024

I could not reproduce with node v22.7.0 on windows:

PS C:\Users\MarcoHD\Documents\projects\forks\node> node --experimental-strip-types --import file:///C:/Users/MarcoHD/Documents/projects/forks/node/test/fixtures/typescript/ts/test-loader.ts  test\fixtures\typescript\ts\test-typescript.ts 
(node:2428) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2428) [MODULE_TYPELESS_PACKAGE_JSON] Warning: file:///C:/Users/MarcoHD/Documents/projects/forks/node/test/fixtures/typescript/ts/test-loader.ts parsed as an ES module because module syntax was detected; to avoid the performance penalty of syntax detection, add "type": "module" to C:\package.json        
(node:2428) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2428) [MODULE_TYPELESS_PACKAGE_JSON] Warning: file:///C:/Users/MarcoHD/Documents/projects/forks/node/test/fixtures/typescript/ts/hook.ts parsed as an ES module because module syntax was detected; to avoid the performance penalty of syntax detection, add "type": "module" to C:\package.json
Hello, TypeScript!

Warns are emitted twice because loader is off thread.
you can find the test here: #54657

@wojtekmaj
Copy link
Author

wojtekmaj commented Aug 30, 2024

Can you run it again with NODE_DEBUG set to * in your env?

Sure!

node --import=./register.ts --experimental-strip-types ./test.js PS C:\server> $Env:NODE_DEBUG = "*" PS C:\server> node --import=./register.ts --experimental-strip-types ./test.js (node:32848) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time (Use `node --trace-warnings ...` to show where the warning was created) ESM 32848: Storing file:///C:/server/register.ts (implicit type) in ModuleLoadMap ESM 32848: async addJobsToDependencyGraph() file:///C:/server/register.ts ModuleJob { importAttributes: [Object: null prototype] {}, isMain: false, inspectBrk: false, url: 'file:///C:/server/register.ts', module: Promise { }, modulePromise: Promise { }, linked: Promise { }, instantiated: undefined } ESM 32848: Translating TypeScript file:///C:/server/register.ts ESM 32848: Translating StandardModule file:///C:/server/register.ts ESM 32848: Storing node:module (implicit type) in ModuleLoadMap ESM 32848: async link() file:///C:/server/register.ts -> node:module ModuleJob { importAttributes: [Object: null prototype] {}, isMain: false, inspectBrk: false, url: 'node:module', module: Promise { }, modulePromise: Promise { }, linked: Promise { }, instantiated: undefined } ESM 32848: Translating BuiltinModule node:module MODULE 32848: load built-in module node:module ESM 32848: Loading BuiltinModule node:module ESM 32848: async addJobsToDependencyGraph() file:///C:/server/register.ts ModuleJob { importAttributes: [Object: null prototype] {}, isMain: false, inspectBrk: false, url: 'node:module', module: ModuleWrap { url: 'node:module' }, modulePromise: Promise { ModuleWrap { url: 'node:module' } }, linked: Promise { [Array(0): null prototype] [] }, instantiated: undefined } WORKER 32848: [0] create new worker internal/modules/esm/worker { stderr: false, stdin: false, stdout: false, trackUnmanagedFds: false, workerData: { lock: SharedArrayBuffer { [Uint8Contents]: <00 00 00 00>, byteLength: 4 } } } isInternal: true WORKER 32848: instantiating Worker. url: node:internal/modules/esm/worker doEval: internal WORKER 32848: [0] created Worker with ID 1 STREAM 32848: pipe count=1 opts=undefined STREAM 32848: resume STREAM 32848: pipe count=1 opts=undefined STREAM 32848: resume ESM 32848: wait for signal from worker ESM 32848: post sync message to worker { method: 'register', args: [ './hooks.ts', 'file:///C:/server/register.ts', undefined, undefined ], transferList: undefined } ESM 32848: wait for sync response from worker { method: 'register', args: [ './hooks.ts', 'file:///C:/server/register.ts', undefined, undefined ] } ESM 32848: wait for sync response from worker { method: 'register', args: [ './hooks.ts', 'file:///C:/server/register.ts', undefined, undefined ] }

Which is vastly different output than when using JS loader:

node --import=./register.js --experimental-strip-types ./test.js PS C:\server> node --import=./register.js --experimental-strip-types ./test.js (node:23116) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time (Use `node --trace-warnings ...` to show where the warning was created) ESM 23116: Storing file:///C:/server/register.js (implicit type) in ModuleLoadMap ESM 23116: async addJobsToDependencyGraph() file:///C:/server/register.js ModuleJob { importAttributes: [Object: null prototype] {}, isMain: false, inspectBrk: false, url: 'file:///C:/server/register.js', module: Promise { }, modulePromise: Promise { }, linked: Promise { }, instantiated: undefined } ESM 23116: Translating StandardModule file:///C:/server/register.js ESM 23116: Storing node:module (implicit type) in ModuleLoadMap ESM 23116: async link() file:///C:/server/register.js -> node:module ModuleJob { importAttributes: [Object: null prototype] {}, isMain: false, inspectBrk: false, url: 'node:module', module: Promise { }, modulePromise: Promise { }, linked: Promise { }, instantiated: undefined } ESM 23116: Translating BuiltinModule node:module MODULE 23116: load built-in module node:module ESM 23116: Loading BuiltinModule node:module ESM 23116: async addJobsToDependencyGraph() file:///C:/server/register.js ModuleJob { importAttributes: [Object: null prototype] {}, isMain: false, inspectBrk: false, url: 'node:module', module: ModuleWrap { url: 'node:module' }, modulePromise: Promise { ModuleWrap { url: 'node:module' } }, linked: Promise { [Array(0): null prototype] [] }, instantiated: undefined } WORKER 23116: [0] create new worker internal/modules/esm/worker { stderr: false, stdin: false, stdout: false, trackUnmanagedFds: false, workerData: { lock: SharedArrayBuffer { [Uint8Contents]: <00 00 00 00>, byteLength: 4 } } } isInternal: true WORKER 23116: instantiating Worker. url: node:internal/modules/esm/worker doEval: internal WORKER 23116: [0] created Worker with ID 1 STREAM 23116: pipe count=1 opts=undefined STREAM 23116: resume STREAM 23116: pipe count=1 opts=undefined STREAM 23116: resume ESM 23116: wait for signal from worker ESM 23116: post sync message to worker { method: 'register', args: [ './hooks.js', 'file:///C:/server/register.js', undefined, undefined ], transferList: undefined } ESM 23116: wait for sync response from worker { method: 'register', args: [ './hooks.js', 'file:///C:/server/register.js', undefined, undefined ] } ESM 23116: wait for sync response from worker { method: 'register', args: [ './hooks.js', 'file:///C:/server/register.js', undefined, undefined ] } ESM 23116: got sync response from worker { method: 'register', args: [ './hooks.js', 'file:///C:/server/register.js', undefined, undefined ] } ESM 23116: post async message to worker { method: 'resolve', args: [ 'file:///C:/server/test.js', undefined, [Object: null prototype] {} ], transferList: undefined } ESM 23116: wait for async response from worker { method: 'resolve', args: [ 'file:///C:/server/test.js', undefined, [Object: null prototype] {} ] } ESM 23116: wait for async response from worker { method: 'resolve', args: [ 'file:///C:/server/test.js', undefined, [Object: null prototype] {} ] } STREAM 23116: resume false STREAM 23116: read 0 STREAM 23116: need readable false STREAM 23116: length less than watermark true STREAM 23116: do read STREAM 23116: flow STREAM 23116: read undefined STREAM 23116: need readable true STREAM 23116: length less than watermark true STREAM 23116: reading, ended or constructing false STREAM 23116: resume false STREAM 23116: read 0 STREAM 23116: need readable false STREAM 23116: length less than watermark true STREAM 23116: do read STREAM 23116: flow STREAM 23116: read undefined STREAM 23116: need readable true STREAM 23116: length less than watermark true STREAM 23116: reading, ended or constructing false STREAM 23116: push WORKER 23116: [1] is setting up worker child environment

STREAM 23116: ondata
WORKER 23116: [1] is setting up worker child environment
STREAM 23116: dest.write true
STREAM 23116: maybeReadMore read 0
STREAM 23116: read 0
STREAM 23116: need readable true
STREAM 23116: length less than watermark true
STREAM 23116: do read
STREAM 23116: push STREAM 23116: push null

STREAM 23116: ondata
STREAM 23116: push null
STREAM 23116: dest.write true
STREAM 23116: push STREAM 23116: onEofChunk

STREAM 23116: ondata
STREAM 23116: onEofChunk
STREAM 23116: dest.write true
STREAM 23116: push STREAM 23116: emitReadable

STREAM 23116: ondata
STREAM 23116: emitReadable
STREAM 23116: dest.write true
STREAM 23116: push STREAM 23116: emitReadable false

STREAM 23116: ondata
STREAM 23116: emitReadable false
STREAM 23116: dest.write true
STREAM 23116: push WORKER 23116: [1] starts worker script internal/modules/esm/worker (eval = internal) at cwd = C:\server

STREAM 23116: ondata
WORKER 23116: [1] starts worker script internal/modules/esm/worker (eval = internal) at cwd = C:\server
STREAM 23116: dest.write true
STREAM 23116: push (node:23116) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)

STREAM 23116: ondata
(node:23116) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)
STREAM 23116: dest.write true
STREAM 23116: push STREAM 23116: emitReadable_

STREAM 23116: ondata
STREAM 23116: emitReadable_
STREAM 23116: dest.write true
STREAM 23116: push STREAM 23116: flow

STREAM 23116: ondata
STREAM 23116: flow
STREAM 23116: dest.write true
STREAM 23116: push ESM 23116: Storing file:///C:/server/hooks.js (implicit type) in ModuleLoadMap

STREAM 23116: ondata
ESM 23116: Storing file:///C:/server/hooks.js (implicit type) in ModuleLoadMap
STREAM 23116: dest.write true
STREAM 23116: push ESM 23116: async addJobsToDependencyGraph() file:///C:/server/hooks.js ModuleJob {
importAttributes: [Object: null prototype] {},
isMain: false,
inspectBrk: false,
url: 'file:///C:/server/hooks.js',
module: Promise { },
modulePromise: Promise { },
linked: Promise { },
instantiated: undefined
}

STREAM 23116: ondata
ESM 23116: async addJobsToDependencyGraph() file:///C:/server/hooks.js ModuleJob {
importAttributes: [Object: null prototype] {},
isMain: false,
inspectBrk: false,
url: 'file:///C:/server/hooks.js',
module: Promise { },
modulePromise: Promise { },
linked: Promise { },
instantiated: undefined
}
STREAM 23116: dest.write true
STREAM 23116: push ESM 23116: Translating StandardModule file:///C:/server/hooks.js

STREAM 23116: ondata
ESM 23116: Translating StandardModule file:///C:/server/hooks.js
STREAM 23116: dest.write true
STREAM 23116: push ESM 23116: Storing node:path (implicit type) in ModuleLoadMap

STREAM 23116: ondata
ESM 23116: Storing node:path (implicit type) in ModuleLoadMap
STREAM 23116: dest.write true
STREAM 23116: push ESM 23116: async link() file:///C:/server/hooks.js -> node:path ModuleJob {
importAttributes: [Object: null prototype] {},
isMain: false,
inspectBrk: false,
url: 'node:path',
module: Promise { },
modulePromise: Promise { },
linked: Promise { },
instantiated: undefined
}

STREAM 23116: ondata
ESM 23116: async link() file:///C:/server/hooks.js -> node:path ModuleJob {
importAttributes: [Object: null prototype] {},
isMain: false,
inspectBrk: false,
url: 'node:path',
module: Promise { },
modulePromise: Promise { },
linked: Promise { },
instantiated: undefined
}
STREAM 23116: dest.write true
STREAM 23116: push ESM 23116: Translating BuiltinModule node:path

STREAM 23116: ondata
ESM 23116: Translating BuiltinModule node:path
STREAM 23116: dest.write true
STREAM 23116: push MODULE 23116: load built-in module node:path

STREAM 23116: ondata
MODULE 23116: load built-in module node:path
STREAM 23116: dest.write true
STREAM 23116: push ESM 23116: Loading BuiltinModule node:path

STREAM 23116: ondata
ESM 23116: Loading BuiltinModule node:path
STREAM 23116: dest.write true
STREAM 23116: push ESM 23116: async addJobsToDependencyGraph() file:///C:/server/hooks.js ModuleJob {
importAttributes: [Object: null prototype] {},
isMain: false,
inspectBrk: false,
url: 'node:path',
module: ModuleWrap { url: 'node:path' },
modulePromise: Promise { ModuleWrap { url: 'node:path' } },
linked: Promise { [Array(0): null prototype] [] },
instantiated: undefined
}

STREAM 23116: ondata
ESM 23116: async addJobsToDependencyGraph() file:///C:/server/hooks.js ModuleJob {
importAttributes: [Object: null prototype] {},
isMain: false,
inspectBrk: false,
url: 'node:path',
module: ModuleWrap { url: 'node:path' },
modulePromise: Promise { ModuleWrap { url: 'node:path' } },
linked: Promise { [Array(0): null prototype] [] },
instantiated: undefined
}
STREAM 23116: dest.write true
STREAM 23116: maybeReadMore read 0
STREAM 23116: read 0
STREAM 23116: need readable true
STREAM 23116: length less than watermark true
STREAM 23116: do read
ESM 23116: got async response from worker {
method: 'resolve',
args: [
'file:///C:/server/test.js',
undefined,
[Object: null prototype] {}
]
} Int32Array(1) [ 5 ]
ESM 23116: post async message to worker {
method: 'load',
args: [
'file:///C:/server/test.ts',
{
format: 'module-typescript',
importAttributes: [Object: null prototype] {}
}
],
transferList: undefined
}
ESM 23116: wait for async response from worker {
method: 'load',
args: [
'file:///C:/server/test.ts',
{
format: 'module-typescript',
importAttributes: [Object: null prototype] {}
}
]
}
ESM 23116: Storing file:///C:/server/test.ts (implicit type) in ModuleLoadMap
ESM 23116: async addJobsToDependencyGraph() file:///C:/server/test.ts ModuleJob {
importAttributes: [Object: null prototype] {},
isMain: true,
inspectBrk: false,
url: 'file:///C:/server/test.ts',
module: Promise { },
modulePromise: Promise { },
linked: Promise { },
instantiated: undefined
}
ESM 23116: got async response from worker {
method: 'load',
args: [
'file:///C:/server/test.ts',
{
format: 'module-typescript',
importAttributes: [Object: null prototype] {}
}
]
} Int32Array(1) [ 7 ]
ESM 23116: Translating TypeScript file:///C:/server/test.ts
ESM 23116: Translating StandardModule file:///C:/server/test.ts
Hello, world!

@wojtekmaj
Copy link
Author

I could not reproduce with node v22.7.0 on windows:

On Windows arm64? :)

@wojtekmaj
Copy link
Author

Fun facts:

  • register.ts that registers hooks.js also works fine
  • hooks.ts does not work under ANY circumstances, even if I strip types off it so it's essentially pure JavaScript

@wojtekmaj wojtekmaj changed the title On Windows, unlike macOS --import can't be a TS file when used with --experimental-strip-types On Windows (arm64?) --import can't be a TS file when used with --experimental-strip-types Aug 30, 2024
@marco-ippolito
Copy link
Member

marco-ippolito commented Aug 31, 2024

try to add change hook.ts to

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
   throw new Error('Kaboom')
};

I tested it on windows x64 I think this an arm64 specific issue.
I noticed weird behavior on arm64 in the past (cc @legendecas) but I dont have a machine to reproduce

@wojtekmaj
Copy link
Author

wojtekmaj commented Aug 31, 2024

@marco-ippolito Both JS and TS throw! O_O

PS C:\server> node --import=./register.js --experimental-strip-types ./src/index.js
(node:26300) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:26300) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

node:internal/modules/run_main:123
    triggerUncaughtException(
    ^
Error: Kaboom!
    at resolve (file:///C:/server/hooks.js:6:9)
    at nextResolve (node:internal/modules/esm/hooks:748:28)
    at Hooks.resolve (node:internal/modules/esm/hooks:240:30)
    at handleMessage (node:internal/modules/esm/worker:199:24)
    at Immediate.checkForMessages (node:internal/modules/esm/worker:141:28)
    at process.processImmediate (node:internal/timers:491:21)

Node.js v22.7.0
PS C:\server> node --import=./register.ts --experimental-strip-types ./src/index.js
(node:23316) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:23316) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

node:internal/modules/run_main:123
    triggerUncaughtException(
    ^
Error: Kaboom!
    at resolve (file:///C:/server/hooks.ts:8:9)
    at nextResolve (node:internal/modules/esm/hooks:748:28)
    at Hooks.resolve (node:internal/modules/esm/hooks:240:30)
    at handleMessage (node:internal/modules/esm/worker:199:24)
    at Immediate.checkForMessages (node:internal/modules/esm/worker:141:28)
    at process.processImmediate (node:internal/timers:491:21)

Node.js v22.7.0
PS C:\server> 

@targos
Copy link
Member

targos commented Aug 31, 2024

I confirm it is arm64-specific:

PS C:\Users\mzasso\Documents\winbug> node -p "os.arch()"
arm64

PS C:\Users\mzasso\Documents\winbug> node --import=./register.ts --experimental-strip-types ./test.ts
(node:5008) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

PS C:\Users\mzasso\Documents\winbug> .\node-x64.exe --import=./register.ts --experimental-strip-types ./test.ts
(node:7584) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node-x64 --trace-warnings ...` to show where the warning was created)
(node:7584) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node-x64 --trace-warnings ...` to show where the warning was created)
Hello, world!

@targos targos added the arm Issues and PRs related to the ARM platform. label Aug 31, 2024
@wojtekmaj
Copy link
Author

wojtekmaj commented Aug 31, 2024

Okay, this is getting interesting. My code gets evaluated, but only... The first line?

import type { ResolveHook } from 'node:module';

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
  console.log('One');
  console.log('Two');
  return nextResolve(specifier, context);
};
S C:\server> node --import=./register.ts --experimental-strip-types ./src/index.js
(node:33800) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:33800) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
One

node:internal/modules/run_main:123
    triggerUncaughtException(
    ^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\server\src\index.js' imported from C:\server\
    at finalizeResolution (node:internal/modules/esm/resolve:257:11)
    at moduleResolve (node:internal/modules/esm/resolve:914:10)
    at defaultResolve (node:internal/modules/esm/resolve:1038:11)
    at nextResolve (node:internal/modules/esm/hooks:748:28)
    at resolve (file:///C:/server/hooks.ts:6:10)
    at nextResolve (node:internal/modules/esm/hooks:748:28)
    at Hooks.resolve (node:internal/modules/esm/hooks:240:30)
    at handleMessage (node:internal/modules/esm/worker:199:24)
    at Immediate.checkForMessages (node:internal/modules/esm/worker:141:28)
    at process.processImmediate (node:internal/timers:491:21) {
  code: 'ERR_MODULE_NOT_FOUND',
  url: 'file:///C:/server/src/index.js'
}

Node.js v22.7.0
PS C:\server>

BUT:

import type { ResolveHook } from 'node:module';

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
  console.log('One'); console.log('Two');
  return nextResolve(specifier, context);
};
S C:\server> node --import=./register.ts --experimental-strip-types ./src/index.ts
(node:35016) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:35016) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
One
Two
One

node:internal/modules/run_main:123
    triggerUncaughtException(
    ^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\server\src\app.js' imported from C:\\server\src\index.ts
    at finalizeResolution (node:internal/modules/esm/resolve:257:11)
    at moduleResolve (node:internal/modules/esm/resolve:914:10)
    at defaultResolve (node:internal/modules/esm/resolve:1038:11)
    at nextResolve (node:internal/modules/esm/hooks:748:28)
    at resolve (file:///C:/server/hooks.ts:5:10)
    at nextResolve (node:internal/modules/esm/hooks:748:28)
    at Hooks.resolve (node:internal/modules/esm/hooks:240:30)
    at MessagePort.handleMessage (node:internal/modules/esm/worker:199:24)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:816:20)
    at MessagePort.<anonymous> (node:internal/per_context/messageport:23:28) {
  code: 'ERR_MODULE_NOT_FOUND',
  url: 'file:///C:/server/src/app.js'
}

Node.js v22.7.0
PS C:\server> 

And more:

  • Adding console.log('One'); console.log('Two'); console.log('Three'); console.log('Four'); evaluates all console.logs + calls nextResolve.
  • Adding also console.log('Five') to the line causes nextResolve not to be called anymore.
  • Adding console.log('Six'), ..., console.log('Ten') still evaluates all console.logs, but nextResolve is obviously still not called.
  • It's not about the length; console.log( '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890', ); is fine, nextResolve gets called.

@wojtekmaj wojtekmaj changed the title On Windows (arm64?) --import can't be a TS file when used with --experimental-strip-types On Windows arm64 --import can't be a TS file when used with --experimental-strip-types Aug 31, 2024
@avivkeller
Copy link
Member

avivkeller commented Aug 31, 2024

When you say you can't reproduce on MacOS, is the Mac also arm64?

Essentially, is this an arm issue or a windows+arm issue?

@wojtekmaj
Copy link
Author

Essentially, is this an arm issue or a windows+arm issue?

Yup, it seems like so. The other device I'm testing on is a M2 MacBook Air.

@wojtekmaj wojtekmaj changed the title On Windows arm64 --import can't be a TS file when used with --experimental-strip-types On Windows arm64 custom loader can't be a TS file when used with --experimental-strip-types Aug 31, 2024
@marco-ippolito
Copy link
Member

marco-ippolito commented Aug 31, 2024

Have you tried without the strip-types flag (js only) if you can reproduce?

@wojtekmaj
Copy link
Author

I haven't; the whole point of my custom loader was to run a TS app, so I can't run it without the flag (or another loader like tsx). I'll try to create a test setup, but it will take me some time!

@marco-ippolito
Copy link
Member

marco-ippolito commented Aug 31, 2024

Apparently tests seem to pass
#54657 on main so either this is fixed on main or test do not cover the case correctly

@avivkeller
Copy link
Member

avivkeller commented Aug 31, 2024

Apparently tests seem to pass

I'm looking at the CI run, I see windows builds, and I see arm64 builds, but is there windows + arm64 runner? (Maybe I'm just not seeing it)

Edit: https://ci.nodejs.org/job/node-test-binary-windows-js-suites/RUN_SUBSET=0,nodes=win11-arm64-COMPILED_BY-vs2019-arm64/29775/

@wojtekmaj do the tests @marco-ippolito added pass on your machine?

@wojtekmaj
Copy link
Author

@redyetidev Yes they do!

image

@marco-ippolito
Copy link
Member

marco-ippolito commented Aug 31, 2024

Ok so it's probably not reproducing the issue 🤔 maybe adding a console.log (or whatever async task) breaks it?
@wojtekmaj can you add a console.log before the 'nextResolve'?

@wojtekmaj
Copy link
Author

wojtekmaj commented Aug 31, 2024

Doesn't seem to break it 🫠

✅ Works:

import type { ResolveHook } from "node:module";

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
	return nextResolve(specifier, context);
};

✅ Works:

import path from "node:path";

import type { ResolveHook } from "node:module";

const tsExts = new Set([".ts", ".tsx", ".mts", ".cts"]);

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
	return nextResolve(specifier, context);
};

✅ Works:

import path from "node:path";

import type { ResolveHook } from "node:module";

const tsExts = new Set([".ts", ".tsx", ".mts", ".cts"]);

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
	const ext = path.extname(specifier);
	console.log(ext);
	return nextResolve(specifier, context);
};

✅ Works:

import path from "node:path";

import type { ResolveHook } from "node:module";

const tsExts = new Set([".ts", ".tsx", ".mts", ".cts"]);

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
	const ext = path.extname(specifier);
	console.log(ext);
	console.log("One");
	console.log("Two");
	console.log("Three");
	return nextResolve(specifier, context);
};

❌ Does NOT work:

import path from "node:path";

import type { ResolveHook } from "node:module";

const tsExts = new Set([".ts", ".tsx", ".mts", ".cts"]);

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
	const ext = path.extname(specifier);
	console.log(ext);
	if (true) {
	}
	return nextResolve(specifier, context);
};

❌ Does NOT work:

import path from "node:path";

import type { ResolveHook } from "node:module";

const tsExts = new Set([".ts", ".tsx", ".mts", ".cts"]);

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
	const ext = path.extname(specifier);
	console.log(ext);
	if (ext !== ".js") {
		console.log("WTF?");
	}
	return nextResolve(specifier, context);
};

❌ Does NOT work:

import path from "node:path";

import type { ResolveHook } from "node:module";

const tsExts = new Set([".ts", ".tsx", ".mts", ".cts"]);

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
	const ext = path.extname(specifier);
	console.log(ext);
	if (ext !== ".js") {
		return nextResolve(specifier, context);
	}
	return nextResolve(specifier, context);
};

❌ Does NOT work:

import path from "node:path";

import type { ResolveHook } from "node:module";

const tsExts = new Set([".ts", ".tsx", ".mts", ".cts"]);

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
	const ext = path.extname(specifier);
	console.log(ext);
	if (ext !== ".js") {
		return nextResolve(specifier, context);
	}
	for (const tsExt of tsExts) {
		const specifierWithExtReplaced = specifier.replace(ext, tsExt);

		try {
			// Try and resolve file using each of TS extensions
			return await nextResolve(specifierWithExtReplaced, context);
		} catch {}
	}
	return nextResolve(specifier, context);
};

✅ Works (!!!):

import path from "node:path";

import type { ResolveHook } from "node:module";

const tsExts = new Set([".ts", ".tsx", ".mts", ".cts"]);

export const resolve: ResolveHook = async function resolve(specifier, context, nextResolve) {
	const ext = path.extname(specifier);
	console.log(ext);
	for (const tsExt of tsExts) {
		const specifierWithExtReplaced = specifier.replace(ext, tsExt);

		try {
			// Try and resolve file using each of TS extensions
			return await nextResolve(specifierWithExtReplaced, context);
		} catch {}
	}
	return nextResolve(specifier, context);
};

@wojtekmaj
Copy link
Author

Ok, final conclusion: any kind of if() breaks it. 🤯

Please note that the last example from my previous comment does NOT fix #54665 - it's still reproducible even though the removal of if() makes THIS issue not reproducible anymore.

@marco-ippolito
Copy link
Member

Wow this is extremely weird 💀 I have no idea why only on Windows arm64 and not on other platforms. Maybe a loaders wizard like @joyeecheung might have an idea

@wojtekmaj
Copy link
Author

Guys, I got it. Kind of.
I looked into #53725 and saw we're using amaro to transform TS types.
I've set it up and added the simplest script possible:

import fs from "node:fs";
import amaro from "amaro";

console.log("Begin");
console.log(amaro.transformSync(fs.readFileSync("./hook.ts", "utf-8")).code);
console.log("End");

Guess what?

Output on hook.ts without if():

PS C:\Users\wojte\Projekty\test> node amaro.js
Begin
import path from "node:path";



const tsExts = new Set([".ts", ".tsx", ".mts", ".cts"]);

export const resolve              = async function resolve(
        specifier,
        context,
        nextResolve,
) {
        const ext = path.extname(specifier);
        console.log(ext);
        for (const tsExt of tsExts) {
                const specifierWithExtReplaced = specifier.replace(ext, tsExt);

                try {
                        // Try and resolve file using each of TS extensions
                        return await nextResolve(specifierWithExtReplaced, context);
                } catch {}
        }
        return nextResolve(specifier, context);
};

End

Output on hook.ts with if():

PS C:\Users\wojte\Projekty\test> node amaro.js
Begin

Yep that's the whole thing.

@avivkeller
Copy link
Member

Is this weird type-stripping exclusive to windows arm64?

@wojtekmaj
Copy link
Author

wojtekmaj commented Aug 31, 2024

Yes. Just copied the whole test repo using OneDrive to my MacBook Air, executed the same command there, and sure enough, it transforms just fine.

image

@avivkeller
Copy link
Member

This is so strange...

FYI @wojtekmaj opened an issue against SWC swc-project/swc#9520

@joyeecheung
Copy link
Member

Maybe try —no-maglev and see if it makes a difference?

@wojtekmaj
Copy link
Author

@joyeecheung Absolutely none.

PS C:\Users\wojte\OneDrive\Projekty\test> node --import=./loaderts.ts --experimental-strip-types ./simple.js
(node:26468) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
PS C:\Users\wojte\OneDrive\Projekty\test> node --import=./loaderts.ts --experimental-strip-types --no-maglev ./simple.js
(node:24364) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

@joyeecheung
Copy link
Member

Then I guess this needs someone who knows enough about what the off-thread hooks API is doing to debug (I don’t)

@joyeecheung
Copy link
Member

By the way, AFIAK console.log() output is not guaranteed to work in the off-thread loader, it can be lost silently. There was also some recent report about structured clone errors being swallowed silently by @JakobJingleheimer

@wojtekmaj
Copy link
Author

I figure there's nothing we can do right now until the SWC bug I reported is fixed. Then, we would need to get SWC updated in Amaro, and finally, get Amaro updated in Node.js.

@aduh95
Copy link
Contributor

aduh95 commented Sep 2, 2024

The issue should probably be renamed if this has nothing to do with custom loader, and maybe we should transfer it to amaro repo? Or am I missing something?

@wojtekmaj
Copy link
Author

Well, it's blocked by Amaro, which in turn is blocked by SWC. But after Amaro is eventually updated with a fixed SWC version, a final push is still needed on Node.js side to get this issue resolved.

@aduh95
Copy link
Contributor

aduh95 commented Sep 2, 2024

What about renaming the issue though?

@aduh95 aduh95 added dependencies Pull requests that update a dependency file. and removed repro-exists loaders Issues and PRs related to ES module loaders labels Sep 2, 2024
@wojtekmaj wojtekmaj changed the title On Windows arm64 custom loader can't be a TS file when used with --experimental-strip-types On Windows arm64 TS-based custom loader sometimes causes the process to exit silently when used with --experimental-strip-types Sep 2, 2024
@marco-ippolito
Copy link
Member

Apparently now its working
#54657
I just got a windows arm64 VM to double check

nodejs-github-bot pushed a commit that referenced this issue Dec 23, 2024
PR-URL: #54657
Refs: #54645
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Zeyu "Alex" Yang <himself65@outlook.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
@wojtekmaj
Copy link
Author

wojtekmaj commented Dec 23, 2024

@marco-ippolito What is the first version on which it's expected to work properly? I'll test on a real machine. Latest LTS, 22.12.0, still has this bug. Moreover, the bug in SWC that I originally filed (and is the cause of this failure) still can be reproduced.

@marco-ippolito
Copy link
Member

@marco-ippolito What is the first version on which it's expected to work properly? I'll test on a real machine. Latest LTS, 22.12.0, still has this bug. Moreover, the bug in SWC that I originally filed (and is the cause of this failure) still can be reproduced.

Try the latest 23, on main the issue is fixed. Today I will provide more info

@marco-ippolito
Copy link
Member

marco-ippolito commented Dec 24, 2024

Tested it on main and v23.5.0, everything works fine, commented here: swc-project/swc#9520 (comment)
Whatever it was it has been fixed, it's hard to bisect since compiling on my Windows ARM64 VM takes hours, (I went to sleep and woke up with node compiled). Unless someone wants to invest more money/better machines (which I'm already doing) into finding the commit that caused this issue, I consider this issue fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
arm Issues and PRs related to the ARM platform. dependencies Pull requests that update a dependency file. strip-types Issues or PRs related to strip-types support windows Issues and PRs related to the Windows platform.
Projects
None yet
Development

No branches or pull requests

6 participants