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

import without extension #30927

Closed
umrigar opened this issue Dec 13, 2019 · 32 comments
Closed

import without extension #30927

umrigar opened this issue Dec 13, 2019 · 32 comments

Comments

@umrigar
Copy link

umrigar commented Dec 13, 2019

According to the v 13.x API docs The "type" field defines how .js and extensionless files should be treated within a particular package.json file’s package scope. and The package scope applies not only to initial entry points (node my-app.js) but also to files referenced by import statements and import() expressions.. However, imports without extensions do not seem to work:

$ node --version
v13.3.0
$ cat package.json 
{ "type": "module" }
$ cat mod.js
export default function() { console.log(42); }
$ cat import-with-js-ext.js 
import f from './mod.js';
f();
$ node import-with-js-ext.js  #works
(node:18690) ExperimentalWarning: The ESM module loader is experimental.
42
$ cat import-without-ext.js 
import f from './mod';
f();
$ node import-without-ext.js #does not work
(node:18868) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/esm/default_resolve.js:96
  let url = moduleWrapResolve(specifier, parentURL);
...            ^
Error: Cannot find module ... /mod imported from ... import-without-ext.js
    at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:96:13)
  ...
  code: 'ERR_MODULE_NOT_FOUND'
}
$

Thanks

@jkrems
Copy link
Contributor

jkrems commented Dec 13, 2019

Hey - thanks for the report! I think you misread this part:

extensionless files

What this means is files that don't have an extension (e.g. if mod.js would be renamed to just mod). The string used to import the file still needs to match the actual filename.

@umrigar
Copy link
Author

umrigar commented Dec 13, 2019

Thanks for the quick response. That's a bit different from the behavior in commonjs but no biggie. I'm closing the issue.

@umrigar umrigar closed this as completed Dec 13, 2019
@GeoffreyBooth
Copy link
Member

The docs should probably make this clearer. I can see how someone would be confused.

@jkrems
Copy link
Contributor

jkrems commented Dec 13, 2019

Thanks for trying out this feature, feedback is super valuable at this point. Please let us know if there's any other issues you're running into!

@Boorj
Copy link

Boorj commented Jan 19, 2020

Turns out that people are really confused with this feature : https://stackoverflow.com/questions/56723678/node-experimental-modules-error-cannot-find-module
That sof post helped to quickly find out that flag --es-module-specifier-resolution=node helps to solve the problem.

@dandv
Copy link
Contributor

dandv commented Feb 7, 2020

The string used to import the file still needs to match the actual filename.

Is that still the case?

This ES modules spec suggests that a list of extensions is automatically tried (via this SO question).

@devsnek
Copy link
Member

devsnek commented Feb 7, 2020

That is the case. The spec you linked to is three years outdated. Please reference https://nodejs.org/api/esm.html in the future.

@BrenoMazieiro
Copy link

In manual there is a specific topic about it: https://nodejs.org/api/esm.html#esm_mandatory_file_extensions

@machineghost
Copy link

That link doesn't really explain anything. It just says:

This behavior matches how import behaves in browser environments, assuming a typically configured server.

Well great ... but if we're not in a browser environment: why force us to use .js then?

@piranna
Copy link
Contributor

piranna commented Jan 23, 2021

but if we're not in a browser environment: why force us to use .js then?

Because ESM modules is a feature available in browsers too, and you could someday to try to use your module there... and in will crash. Maybe is a thing that should be dictated by the spec.

@ljharb
Copy link
Member

ljharb commented Jan 23, 2021

@piranna no, it wouldn't. browsers don't have the concept of "extensions". You'll just need to ensure (as you always would) that the URLs work - for ESM, either with import maps, or by teaching your server how to map the nice clean paths into a file on the filesystem.

@machineghost
Copy link

machineghost commented Jan 24, 2021

I just want to point out that there are two separate things here. One is the Node org making a call (right or wrong) on how to handle extensions ... and the other is the Node org making a call whether or not to explain that decision.

Maybe the Node org made the right call on extensions ... but to an outside observer, it's impossible to tell, because the Node org refuses to explain why they made that call.

In this thread I think all anyone is asking for (or at least all I'm asking for) is for the Node org to update the documentation to explain why this (clearly unpopular) choice was technically necessary ... if it truly was.

@ljharb
Copy link
Member

ljharb commented Jan 24, 2021

It was not technically necessary at all; the desire as i understand it was to match closer with browsers (ie, 1:1 specifiers to lookups, no helpful/magic resolution logic), and to make it easier for tooling to translate a node dependency graph to browser import maps. The latter, however, doesn’t apply anymore since there’s custom conditions, and subpath patterns, not to mention the vast majority of packages still being CJS without an exports field, but here’s where we are.

@GeoffreyBooth
Copy link
Member

No decision has been made. That's why --experimental-specifier-resolution exists. The default is explicit because changing the default to node would not be a breaking change, whereas flipping the other way would be.

I don't expect anything to change until work on loaders leaves experimental status. At that point it should become clear if Node needs to provide a built-in way to resolve extensionless specifiers or if that's something that could be reasonably achieved via external solutions.

@MrBrN197
Copy link

MrBrN197 commented Feb 1, 2021

I've been trying to get es6 imports to work. why is in in guides and tutorials the code usually uses the import syntax without extensions on the files but it seems to still work?

@ljharb
Copy link
Member

ljharb commented Feb 1, 2021

@MrBrN197 because everyone in those examples is using Babel or TypeScript, not native ESM.

@MrBrN197
Copy link

MrBrN197 commented Feb 1, 2021

@ljharb I see. I've just started and I'm trying not to get into Babel. but I use typescript. so typescript compiles to es6 and uses extensions on modules imports.

@piranna
Copy link
Contributor

piranna commented Feb 1, 2021

Typescript uses Babel under the hood.

@ljharb
Copy link
Member

ljharb commented Feb 1, 2021

I don't believe that's true - tsc does its own thing - but you can (and should) transpile TypeScript with Babel as well. Either way, neither requires extensions, only node's native ESM does (browsers lack the concept, so they don't require extensions either)

@merlinstardust
Copy link

In addition to the above, at the very least, node needs to add more helpful error messaging. Adding "type": "module" to my package.json worked until I started doing relative imports. And the only error was that the module could not be found. It was only by chance that after two hours did I find the solution of using extensions or the --experimental-specifier-resolution.

As such, I created an issue for this at #38484

@Shudrum
Copy link

Shudrum commented Aug 5, 2021

Hello!

And what about index scripts?
This feature is quite usefull and readable when you require a "folder".

Now we must do import resources from './resources/index.js, instead of the good old require('./resources').

@Piliponful
Copy link

I don't understand why would anyone decide to change the behavior everyone is already used to and which works for everyone...

@AnderAmorim
Copy link

Set type="module" and change from nodemon src/index.js to nodemon --experimental-modules --es-module-specifier-resolution=node src/index.js worked for me!

@ljharb
Copy link
Member

ljharb commented Apr 3, 2022

@AnderAmorim You don't actually need type module for that; you could also rename src/index.js to src/index.mjs and it should otherwise work the same.

marcindulak added a commit to marcindulak/knowthen-graphql that referenced this issue Jul 10, 2022
@IgorMing
Copy link

Set type="module" and change from nodemon src/index.js to nodemon --experimental-modules --es-module-specifier-resolution=node src/index.js worked for me!

this worked for me as well. Thank you @AnderAmorim!

@jlguenego
Copy link

How we do in node 19. They have removed the experimental option. I cannot found easily a replacement.

@piranna
Copy link
Contributor

piranna commented Mar 8, 2023

How we do in node 19. They have removed the experimental option. I cannot found easily a replacement.

My solution is to run it with Node.js v18 using nvm... Not the best optimal one, though.

@jlguenego
Copy link

I investigated on node 19: here is my solution.

Use a custom loader (node --loader). It is experimental so you get a warning. Use --no-warnings to disable the experimental warning.

Then I had to write a custom loader script in order to get the .js extension added in certain cases.

node --no-warnings --loader ./dist/esm-loader.js dist/server.js

Here is the script of the esm-loader.js

export const resolve = (specifier, context, nextResolve) => {
  if (specifier.startsWith("./") || specifier.startsWith("../")) {
    if (
      !(
        specifier.endsWith(".js") ||
        specifier.endsWith(".mjs") ||
        specifier.endsWith(".cjs")
      )
    ) {
      const newSpecifier = specifier + ".js";
      return nextResolve(newSpecifier, context);
    }
  }
  return nextResolve(specifier, context);
};

You can see it working on my educational project here:

https://github.com/jlg-formation/njs-mars-2023/tree/50eaf33a3e2d229e3dcb239bfd26e95ba92aae00

@jcalfee
Copy link

jcalfee commented Jul 9, 2023

I'm glad we have a custom loader. We are not suppoto break stuff in the mean time though. It is hard enough to find people to maintain all these libraries out there. Features need to keep working if this is going to be a good investment for developers to use node and JavaScript.

@divmgl
Copy link

divmgl commented Sep 23, 2023

They have removed the experimental option. I cannot found easily a replacement.

What is going on with Node.js? Why has it become a nightmare to use this framework?

Now the latest: I have to now go through tens of thousands of files and change their imports to use a .js suffix (even though I'm using TypeScript). Why? Thankfully my team is on 18, and I found this option, but now it's removed. Why? Not even ESM through Babel transpilation back in 2019 worked like this.

@wintercounter
Copy link

I'm using TS for an NPM package that has an executable. I compile with swc which adds only .js extension. If I specify .js for my imports, my code will run fine, however, I'll completely lose type-safety as TS won't be able to resolve my imports during development, as it's not there. What is the expected solution here? Don't use TS? Seems like I'm pushed more and more towards Bun by Node.

@robey
Copy link
Contributor

robey commented Sep 3, 2024

Here's a slightly more extended version of jlguenego's importer in the comment above, which has been working for me for a few months, and restores extension-free import while the various projects are still fighting it out to see who will blink first.

Put this in fix_imports.mjs and run node with the added option --import ./fix_imports.mjs:

import fs from "node:fs";
import module from "node:module";
import path from "node:path";
import url from "node:url";


process.setSourceMapsEnabled(true);

// register myself as the handler for myself handling
module.register(import.meta.url, url.pathToFileURL("./"));

function isDir(filename) {
    return fs.existsSync(filename) && fs.statSync(filename).isDirectory();
}

function isFile(filename) {
    return fs.existsSync(filename) && !fs.statSync(filename).isDirectory();
}

// fix imports
export async function resolve(specifier, context, next) {
    if (module.isBuiltin(specifier) || !specifier.startsWith(".")) return next(specifier, context);

    if (context.parentURL !== undefined) {
        const dirname = path.dirname(url.fileURLToPath(context.parentURL));
        specifier = path.resolve(dirname, specifier);
    }

    // do the normal-style filename resolution
    if (!isFile(specifier)) {
        if (isDir(specifier) && isFile(`${specifier}/index.js`)) {
            specifier += "/index.js";
        } else if (isFile(`${specifier}.js`)) {
            specifier += ".js";
        }
    }

    return next(specifier, context);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests