node.js 13 - import is not working properly - javascript

$ node --version
v13.8.0
Let's create three files:
// package.json
{
"type": "module"
}
// foobar.js
function foo() { console.log('foo') }
export default {foo}
// index.js
import Foo from './foobar.js';
Foo.foo();
Run index.js
$ node index.js
(node:22768) ExperimentalWarning: The ESM module loader is experimental.
foo
All working.
And now changing './foobar.js'; to './foobar';
// index.js
import Foo from './foobar';
Foo.foo();
And we get an error!
(node:22946) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/esm/resolve.js:58
let url = moduleWrapResolve(specifier, parentURL);
^
Error: Cannot find module /foobar imported from /index.js
There is no other files in directory.
Why does it happens?
Why import without extension doesn't work?
UPDATE:
https://nodejs.org/api/esm.html
package.json "type" field
Files ending with .js or lacking any extension will be loaded as ES modules when the nearest parent package.json file contains a top-level field "type" with a value of "module".
So './foobar' must work.
UPDATE 2:
I believe what the documentation call "extensionless files" are literaly files without extensions, not files imported without extensions.
For example, if you import your file with import Foo from './foobar';, and you file is called foobar without the .js extension, it will work fine.
Thanks to #Seblor

Looking at the documentation, in the category Differences Between ES Modules and CommonJS, and more precisely at the "Mandatory file extensions" section, it says that the .js file extension must be present to make the import work :
A file extension must be provided when using the import keyword. Directory indexes (e.g. './startup/index.js') must also be fully specified.
This behavior matches how import behaves in browser environments, assuming a typically configured server.
I am not sure why your last question is about the import without extention not working anymore. I guess you have been working at some point with a transpiler like babel that will resolve the files without the extensions.
Edit :
I believe what the documentation call "extensionless files" are literaly files without extensions, not files imported without extensions.
For example, if you import your file with import Foo from './foobar';, and you file is called foobar without the .js extension, it will work fine.

Related

How to import own javascript code into an express/node server [duplicate]

I have a file, test.js with these lines of code inside:
import {blabla} from "./bla";
async function dataGenerator() {
..........
}
(async() => {
console.log('1')
await dataGenerator()
console.log('2')
})()
Note: Ignore the import structure. It is just fictive for the question. In my file the imports are auto.
When I'm trying to run from terminal with node test.js it returns error:
Cannot find module 'D:\bla' imported from D:\test.js
I have added into package.json the line: "type": "module". Without this it returns:
Cannot use import statement outside a module
I'm using node v14. How can I run the test.js without adding to all the imports ".js". There are functions in functions in functions and is complicated to add .js extension. Is there any npm to run it?
Node.js by default does not attempt to guess the file extension when using import for ES modules. This is different from CommonJS modules with require.
In the documentation for the ES module loader you can read how files are found on disk.
The heading 'Customizing ESM specifier resolution algorithm' states:
The --experimental-specifier-resolution=[mode] flag can be used to customize the extension resolution algorithm. The default mode is explicit, which requires the full path to a module be provided to the loader. To enable the automatic extension resolution and importing from directories that include an index file use the node mode.

Mark file imported by the main file in an ES6 Node.JS project as a module too?

I just went through the discomforting import vs. require time suck that many of us go through, trying to use both ES6 and CommonJS files in the same project, in a stand-alone Node.js utility I am working on.
I did find the the following solutions that you (hopefully) find at the end of a lengthy Web/Stack Overflow session:
Change the extension of the main file to ".mjs"
OR: Add type: "module" to the project's package.json file
Then add these two lines of alchemy to your project to get back the use of require:
import {createRequire} from "module";
const require = createRequire(import.meta.url);
That all works. Unfortunately, I have legacy files with ".js" extensions in the project that are actually modules. They use the import statement and they also export symbols. This leads right back to that annoying "cannot use import outside a module" error when I ry to run the utility.
Is there a way to use package.json to mark those included files too as modules? I am really loathe to change their extensions to ".mjs" because they are used by a lot of projects and that would lead to fairly massive GitHub headache.
If not, is there some other way to solve this problem?
----- SOURCE CODE SNIPPETS ----
My package.json file for the utility:
{
"name": "utility",
"main": "inspect-animation-model-data-file.js",
"type": "module"
}
Here is the code that imports the problem file (FBXLoader.js), with the createRequire alchemy statements shown:
import {createRequire} from "module";
const require = createRequire(import.meta.url);
import FBXLoader from "../public/javascripts/threejs/examples/jsm/loaders/FBXLoader.js";
Here are the elements in FBXLoadder.js that trigger the "cannot use import" error.
import {
AmbientLight,
AnimationClip,
...
// And...
export { FBXLoader };

When do we need to add file extension when importing JavaScript modules?

When importing modules, I noticed sometimes imported files have their extension, example:
import { myFunc } from './foo.js';
Whereas other libraries, the imports do not:
import { myFunc } from './foo';
Is this related to ES modules vs CommonJS modules?
It depends on your runtime and compilation environment, and also on whether you are using ES modules (the import syntax) or CommonJS modules (the require syntax). Find below an overview of the most common cases:
Webpack (used by Create React App and others tools) works with ES modules like so:
If the path has a file extension, then the file is bundled straightaway.
Otherwise, the file extension is resolved using the resolve.extensions option, which tells the resolver which extensions are acceptable for resolution, e.g. .js, .jsx. More on the official doc.
If you are using ES modules with Node.js or in the browser without any compilation step:
A file extension must be provided when using the import keyword to resolve relative or absolute specifiers... This behavior matches how import behaves in browser environments... More on the official doc.
If you are using CommonJS modules with Node.js:
If the exact filename is not found, then Node.js will attempt to load the required filename with the added extensions: .js, .json, and finally .node. More on the official doc.

Why is Javascript module with ".js" file extension not found on import unless file extension is .mjs despite package "type" being "module"?

Given the following quote from the ECMAScript documentation and minimal reproducible code example,
why does using the .js file extension for an Javascript ES module import cause an ERR_MDOULE_NOT_FOUND error when package.json has "type": "module"?
From Node.js v16.3.0 documentation - Determining module system (emphasis mine)
Determining module system
Node.js will treat the following as ES modules when passed to node as the initial input, or when referenced by import statements within ES module code:
Files ending in .mjs.
Files ending in .js when the nearest parent package.json file contains a top-level "type" field with a value of "module".
The documentation says that a .js file extension is treated as an ES module as long as we declare our package's type as module.
Now consider the following minimal reproducible example of how a .js file does not get treated as an ES Module unless renamed to .mjs.
package.json
{
"type": "module"
}
foo.js
export default 'foo module';
index.js
import foo from './foo';
console.log("Hello", foo);
With the above file names and code, the following error occurs.
$ node index.js
node:internal/process/esm_loader:74
internalBinding('errors').triggerUncaughtException(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/georgep/nodemodulestest/foo' imported from /Users/georgep/nodemodulestest/index.js
Did you mean to import ../foo.js?
at new NodeError (node:internal/errors:363:5)
at finalizeResolution (node:internal/modules/esm/resolve:307:11)
at moduleResolve (node:internal/modules/esm/resolve:742:10)
at Loader.defaultResolve [as _resolve] (node:internal/modules/esm/resolve:853:11)
at Loader.resolve (node:internal/modules/esm/loader:89:40)
at Loader.getModuleJob (node:internal/modules/esm/loader:242:28)
at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:73:40)
at link (node:internal/modules/esm/module_job:72:36) {
code: 'ERR_MODULE_NOT_FOUND'
}
But, if I change the following
change foo.js to foo.mjs, and
Update the import in index.js to reflect foo.mjs => import foo from './foo.mjs';
Then the program executes without error.
Why is the .mjs file ending necessary in this situation, when the documentation clearly states that setting "type": "module" in package.json should mean that node treats regular .js files like ES modules?
Environment:
$ node -v
v16.3.0
As pointed out by #ASDFGerte in their comment, and also fully explained in this answer to "Omit the file extension, ES6 module NodeJS":
In ES Modules, file extension is mandatory, so you cannot omit the .js file extension like you can in CommonJS.
This was the source of my confusion. Once I include the file extension, ES Modules work. For example, this works.
index.js
import foo from './foo.js';
The solution feels pretty obvious, but without knowing the exact reason, this difference between the CommonJS convention and ES Modules would have felt like a bug anyways, so I'm happy to have learned how each module system treats its file extensions.

How to make TypeScript output valid ES6 module import statements?

All major browsers have supported ES6 modules for some time.
These differ from many of the server-side approaches in that they need to specify the exact file to import from - they can't use file discovery.
This makes sense - in Node applications or bundlers like WebPack they only really need the name of the module, and then can spend a bit of extra time discovering the specific file that holds the code. On the web that could be a lot of wasted round trips (is 'library' in library/index.js, or library/library.js, or library.js? require() doesn't care but on the web we have to).
TypeScript has ES6 modules support (set "module": "es6" in tsconfig.json) but it appears to be using a file discovery approach...
Suppose I have library.ts:
export function myFunction(...) { ... }
Then in app.ts:
import {myFunction} from './library';
var x = myFunction(...);
However, this is unchanged when transpiles - the TS output still has the 'library' name for file discovery, which doesn't work. This throws an error because 'library' isn't found:
<script type="module" src="app.js"></script>
In order for ES6 modules to work the TS output needs to reference the specific file:
import {myFunction} from './library.js';
var x = myFunction(...);
How do I make TS output valid ES6 module import statements?
Note: I am not asking how to make a bundler join the TS output into a single file. I specifically want to load these files individually using <script type="module">
This is a bug in TypeScript, though there's some debate about whether it should be fixed.
There is a workaround: while TS won't allow you to specify a .ts file as the source of a module, it will let you specify a .js extension (and then ignore it).
So in app.ts:
import {myFunction} from './library.js';
var x = myFunction(...);
This then outputs correctly in app.js, and TS has found the import definitions and bindings correctly.
This has one advantage/gotcha to be aware/careful of: TS just ignores the .js extension and loads the rest of the path with the usual file discovery. This means that it will import library.ts, but it would also find definition files like library.d.ts or import files in a library/ folder.
That last case might be desirable if you're joining those files together into a library.js output, but to do that you're going to be looking at either lots of nested tsconfig.json files (messy) or possibly the pre-transpiled output of another library.
The compiler takes a module kind flag:
--module ES2015
And you'll also need to be targeting ECMAScript 6 / 2015...
--target ES2015
You need both the module kind and the compilation target to be ECMAScript 2015 minimum to have "zero transformation imports".
Your import statements should look half-way between your two examples:
import {myFunction} from './library';
Additional Notes
There is still clearly a lot of discussion about module resolution... there is the TC39 specification, and the WHATWG specification - plus Node is currently still file-extention-less... looks like RequireJS might live longer than we all thought... please see:
The TypeScript thread for supporting file extensions during import transpilation (i.e. will it add the file extension?).
Recommendation
Stick with a module loader, for example RequireJS or SystemJS. This also means your modules can be shared between browser and server by using UMD or System module kinds repectively.
Obviously, once the ECMAScript discussion reaches a conclusion this will need a revisit.
For a personal project I went the other way. Since I had NPM calling a shell script to copy index.html over to the /build folder, I had the shell script then mass-rename all .js files to have no extension at all.
I did have to inform IIS in "MIME Types" section that an extension-less file should have MIME type application/javascript for that particular site, but it did indeed work. No webpack, no SystemJS, nothing. Index.html just had a hard-coded
<script type="module">
import "./app";
</script>
This was important because I was using a testing framework jest which did not like me putting the .js into the typescript import statements.

Categories