nodejs / node

Node.js JavaScript runtime ✨🐢🚀✨
https://nodejs.org
Other
107.45k stars 29.54k forks source link

Means to detect if the current code is run via `--import` #53882

Open timfish opened 3 months ago

timfish commented 3 months ago

What is the problem this feature will solve?

Code might want to determine if it's running via main app entry point or via a module imported via --import. For example, a library might instruct users to run the code via --import and might want to warn users if this is not the case.

What is the feature you are proposing to solve the problem?

Not sure what the API should be, maybe process.isEntryPoint or something like that?

The entry point is already marked on globalThis via a private symbol: https://github.com/nodejs/node/blob/aca49fc7d16ae87876fec6285b658868e04b1cf7/lib/internal/modules/esm/module_job.js#L255-L257

What alternatives have you considered?

Currently the only way I can think to detect this would be to parse new Error().stack to find the entry point file and then parse and compare to process.execArgv. This is not trivial since you'd need to resolve bare specifiers to their actual source files.

sosoba commented 3 months ago

Code might want to determine if it's running via main app entry point or via a module imported via --import.

You can recognize this via: https://nodejs.org/api/worker_threads.html#workerismainthread

timfish commented 3 months ago

isMainThread is to check for main thread vs worker threads.

It does not tell you if code has been loaded via --import as can be seen from this example:

import.mjs

import { isMainThread } from "worker_threads";
console.log("import.mjs", isMainThread);

app.mjs

import { isMainThread } from "worker_threads";
console.log("app.mjs", isMainThread);
node --import=./import.mjs ./app.mjs
import.mjs true
app.mjs true
RedYetiDev commented 3 months ago

import.js

import { isMainThread } from "worker_threads";
const {
    privateSymbols: {
        entry_point_module_private_symbol,
    },
} = internalBinding('util');

console.log("[IMPORT] worker_threads.isMainThread =", isMainThread);
console.log("[IMPORT] entry_point_module_private_symbol = ", globalThis[entry_point_module_private_symbol])

main.js

import { isMainThread } from "worker_threads";
const {
    privateSymbols: {
        entry_point_module_private_symbol,
    },
} = internalBinding('util');

console.log("[MAIN] worker_threads.isMainThread =", isMainThread);
console.log("[MAIN] entry_point_module_private_symbol = ", globalThis[entry_point_module_private_symbol])

node --expose-internals -r internal/test/binding --import ./import.js main.js
(node:20231) internal/test/binding: These APIs are for internal testing only. Do not use them.
(Use `node --trace-warnings ...` to show where the warning was created)
[IMPORT] worker_threads.isMainThread = true
[IMPORT] entry_point_module_private_symbol =  undefined
[MAIN] worker_threads.isMainThread = true
[MAIN] entry_point_module_private_symbol =  ModuleWrap {
  sourceMapURL: undefined,
  url: 'file:///XYZ/main.js'
}
RedYetiDev commented 3 months ago

@timfish One way to check if your script was imported is to use the cache:

➜ node --import ./import.js main.js
[IMPORT] false
[MAIN] true

import.js

import module from 'node:module';

console.log('[IMPORT]', Object.values(module._pathCache).includes(import.meta.filename))

// Other imports

main.js

import module from 'node:module';

console.log('[MAIN]', Object.values(module._pathCache).includes(import.meta.filename))
timfish commented 3 months ago

Thanks @RedYetiDev!

Is _pathCache documented anywhere and safe to use?

This does have the downside that it's only reliable the first time it's called. Can it be removed from the cache so it's correct every time it's called?

check.mjs

import module from "node:module";

export function check(type) {
  console.log(
    `[${type}]`,
    Object.values(module._pathCache).includes(import.meta.filename)
  );
}

import.mjs

import { check } from "./check.mjs";
check("import");

app.mjs

import { check } from "./check.mjs";
check("app");
node --import ./import.mjs app.mjs
[import] false
[app] false

It looks like this would be easy to add to Node, I'm happy to do a PR if an API can be agreed!

const {
    privateSymbols: {
        entry_point_module_private_symbol,
    },
} = internalBinding('util');

const isEntryPoint = globalThis[entry_point_module_private_symbol])
RedYetiDev commented 3 months ago

I don't know to much about whether the API is stable (because it's used internally) so CC @nodejs/loaders


As for the addition of the API, I believe #49440 discussed something similar. This may be a duplicate of that, I don't really know.

ljharb commented 3 months ago

Does a similar approach work for --require?

GeoffreyBooth commented 3 months ago

I believe #49440 discussed something similar

Yes, I think import.meta.main is the proper solution for this.

timfish commented 3 months ago

Yes, I think import.meta.main is the proper solution for this.

So if import.meta.main was added, this would be undefined in modules loaded via --import?

RedYetiDev commented 3 months ago

Yes

Flarna commented 3 months ago

for --require one can use module.isPreloading, see here

I think a similar API should be added for --import or the existing API should include it.

GeoffreyBooth commented 3 months ago

I think a similar API should be added for --import or the existing API should include it.

That could also work. That's perhaps more appropriate for this particular case because there are questions about what import.meta.main should be for worker entry points and such.

I think we don't need to distinguish between modules loaded via --require or via --import, especially since the latter could load CommonJS or ESM.

ljharb commented 3 months ago

I agree; does module.isPreloading work for --import also?

Flarna commented 3 months ago

Not in my fast local tests in main thread only.

timfish commented 3 months ago

Yes, I think import.meta.main is the proper solution for this.

import.meta.main is only true in the entry module but not for any sub-modules. That means it's not suitable to solve the stated goal of this issue:

a library might instruct users to run the code via --import and might want to warn users if this is not the case

Unless the code is bundled, a library will always read import.meta.main as false.

module.isPreloading is exactly what was looking for. If we can have this to be true for --import too that would be great.

GeoffreyBooth commented 3 months ago

If we can have this to be true for --import too that would be great.

Agreed. In the meantime, I think looking in process.argv and checking the values with import.meta.resolve might provide another solution, but I feel like it probably has holes and edge cases that it'll miss.