I was reading this article about Javascript module:
https://javascript.info/modules-intro#no-bare-modules-allowed
It says
In the browser, import must get either a relative or absolute URL.
Modules without any path are called “bare” modules. Such modules are
not allowed in import.
However, in a lot of documentations that I see, import is being used with "bare" module, this one for example:
https://sheet2api.com/google-sheet-javascript/
What did I miss?
The example you posted is a Node.js example, it installs with the npm package manager
The documentation said "Certain environments, like Node.js or bundle tools allow bare modules" which is true
Related
I am unsure whether I am supposed to use the require version or the import version.
It doesn't state that in the documentation and I found a statement in a Github issue that
Mixing import and require is definitely discouraged. The only way for Rollup to handle require statements is with rollup-plugin-commonjs, but that plugin will skip any files with import or export statements.
which could be interpreted as: "You need to still use require otherwise the common-js plugin will ignore your file and things will not work." or as "always use import everything else would constitute mixing". So that really confused me.
Context
I am trying to import a CommonJS library (Citation-js) into a javascript module (really typescript but I hope this is not relevant here). Now the documentation of common-js tells me to do
const Cite = require('common-js');
which tells me that it is a commonjs library (right?). Therefore I added
import commonjs from "rollup-plugin-commonjs";
import { nodeResolve } from "#rollup/plugin-node-resolve";
to my rollup config and put plugins: [commonjs(), typescript(), nodeResolve()] into the configuration.
Now vscode stops underlining everything and building the website with rollup works again. But the compiled javascript simply states require('common-js') and my browser complains that require is undefined.
Uncaught ReferenceError: require is not defined
So I tried
import Cite from 'common-js';
instead. But that resulted in the rollup build failing with
[!] Error: Unexpected token (Note that you need #rollup/plugin-json to import JSON files)
node_modules/#citation-js/core/package.json (2:8)
1: {
2: "name": "#citation-js/core",
^
Now I could of course install that plugin. But I am not sure that is right, since the whole point of a tool like rollup should be that dependencies of dependencies should be resolved automatically right?
I have also tried
Can't import npm modules in commonjs with rollup : "require is not defined"
This seemed like it would fix my problem: Using Older Require Module With Rollup
But:
the rollup-plugin-node-builtins is apparently not maintained (npm protested with security vulnerabilities and I found this: https://github.com/rollup/rollup/issues/2881). EDIT: There is a new package rollup-plugin-polyfill-node replacing that I guess.
resolve is no longer a member of #rollup/plugin-node-resolve so I assume that this has become nodeResolve which I am already using...
EDIT: installing #rollup/plugin-json actually lets me build the site again (with a bunch of warnings)
(!) Missing shims for Node.js built-ins (which is not fixed by the rollup-plugin-polyfill-node above)
(!) Missing global variable names
(!) Circular dependencies
(!) Unresolved dependencies
not sure what to do about these warnings
This seems to be a trivial problem, but it is not very obvious what settings/configurations need to be used to solve this issue.
Here are the Hello World program directory structure and the source code:
Directory Structure:
| -- HelloWorldProgram
| -- HelloWorld.ts
| -- index.ts
| -- package.json
| -- tsconfig.json
index.ts:
import {HelloWorld} from "./HelloWorld";
let world = new HelloWorld();
HelloWorld.ts:
export class HelloWorld {
constructor(){
console.log("Hello World!");
}
}
package.json:
{
"type": "module",
"scripts": {
"start": "tsc && node index.js"
}
}
Now, execution of the command tsc && node index.js results in the following error:
internal/modules/run_main.js:54
internalBinding('errors').triggerUncaughtException(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'HelloWorld' imported from HelloWorld\index.js
Did you mean to import ../HelloWorld.js?
at finalizeResolution (internal/modules/esm/resolve.js:284:11)
at moduleResolve (internal/modules/esm/resolve.js:662:10)
at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:752:11)
at Loader.resolve (internal/modules/esm/loader.js:97:40)
at Loader.getModuleJob (internal/modules/esm/loader.js:242:28)
at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:50:40)
at link (internal/modules/esm/module_job.js:49:36) {
code: 'ERR_MODULE_NOT_FOUND'
}
It is obvious that the problem seems to have been originated from the fact that in index.ts Typescript file there is no .js extension in the import statement (import {HelloWorld} from "./HelloWorld";). Typescript didn't throw any error during compilation. However, during runtime Node (v14.4.0) wants the .js extension.
Hope the context is clear.
Now, how to change the compiler output setting (tsconfig.json or any flags) so that local relative path imports such as import {HelloWorld} from ./Helloworld; will get replaced by import {HelloWorld} from ./Helloworld.js; during Typescript to Javascript compilation in the index.js file?
Note:
It is possible to directly use the .js extension while importing inside typescript file. However, it doesn't help much while working with hundreds of old typescript modules, because then we have to go back and manually add .js extension. Rather than that for us better solution is to batch rename and remove all the .js extension from all the generated .js filenames at last.
To fellow developers who are looking for a solution to this issue, the possible work-arounds we have come across are as follows:
Use .js extension in the import:
For new files, it is possible to simply add ".js" extension in the import statement in Typescript file while editing.
Example: import {HelloWorld} from "./HelloWorld.js";
Extensionless filename
If working with old projects, rather than going through each and every file and updating the import statements, we found it easier to simply batch rename and remove the ".js" extension from the generated Javascript via a simple automated script. Please note however that this might require a minor change in the server side code to serve these extension-less ".js" files with the proper MIME type to the clients.
Use regex to batch replace import statements
Another option is to use regular expression to batch find and replace in all files the import statements to add the .js extension. An example: https://stackoverflow.com/a/73075563/3330840 or similar other answers.
Updated side note:
Initially, some answers and comments here created unnecessary distractions and tried to evade the original purpose of the question instead of providing possible solutions and dragged me into having to defend the validity of the problem. 16k+ views on this question indicates many developers were faced with this issue as well which itself proves the importance of the question. Hence, the original side note now has been moved to the comments to avoid further distraction.
If you are using VS code, you can use regex to replace the paths.
Find: (\bfrom\s+["']\..*)(["'])
Replace: $1.js$2
This solution is inspired on a previous solution, but this one works better because of the reasons outlined below. Note that this solution is not perfect since it uses regex instead of syntactically analyzing file imports.
Ignores npm imports. Example:
import fs from 'fs'
Supports multi-line imports. Example:
import {
foo,
bar
} from './file'
Supports as imports. Example:
import * as foo from './file'
Supports single and double quotes. Example:
import foo from './file'
import foo from "./file"
Supports exports. See export docs. Example:
export { foo } from './file'
you also can add nodejs CLI flags for enable node module resolution:
for importing json --experimental-json-modules
for importing without extensions --experimental-specifier-resolution=node
node --experimental-specifier-resolution=node dist/some-file.js
It's worth mentioning --experimental-specifier-resolution=node has a bug (or not) then you cannot run bin scripts without extensions (for example in package.json bin section, "tsc" won't work, but "tsc":"tsc.js" will work).
Too many packages have bin scripts without any extensions so there is some trouble with adding NODE_OPTIONS="--experimental-specifier-resolution=node" env variable
As many have pointed out. The reason why TypeScript doesn't and will never add file extension to import statements is their premise that transpiling pure JavaScript code should output the same JavaScript code.
I think having a flag to make TypeScript enforce file extensions in import statements would be the best they could do. Then linters like ESLint could maybe offer an auto fixer based on that rule.
In case you have trouble with TypeScript and ESM, there is this tiny library that actual works perfectly:
npm install #digitak/tsc-esm --save-dev
Replace the tsc call with tsc-esm in your scripts:
{
"scripts": {
"build": "tsc-esm"
}
}
Finally you can run:
npm run build
Had the same issue on a big monorepo, can't edit each file manually,
so I wrote a script to fix all esm import in my project and append .js or /index.js in a safe way:
fix-esm-import-paths
Test before using in your project.
(Probably) Better Answer
See here for a potentially better answer based on this idea & proposed in the comments.
I haven't tested this yet, but seems like my original answer below is lacking & seems like the linked answer is better. I can't say for sure but I'd recommend people check that out first.
My Original Answer
If you know that all your import statements should really have the .js extension, and all imports either have no extension or already have the .js extension, you could use a regex find/replace to "normalise" everything. I would advise you just check your git (or other VCS) logs before committing the change. Here are the regexes I use in VSCode:
Find: (import .* from ".*(?!\.js)(.){3})".
Replace: $1.js".
The find expression will match imports without the .js extension. The first group will capture the part up to the closing quote. The replace expression then takes the first group of the match, which always doesn't have the .js extension, and then appends the extension.
Failing getting a linter set up, you could run this periodically & check the git logs to ensure no imports without the extension slip into the codebase.
npm, anyone?
npm i fix-esm-import-path
check it on npmjs or github.
Only has 8 stars (one is from me), but I'm using it on multiple projects and it does what I need:
npx fix-esm-import-path dist/your-compiled-entrypoint.js
I usually just use the .js extension in import statements in typescript files as well and it works.
Not using a file extension in import paths is a nodejs only thing. Since you are not using commonjs but module you are not using nodejs. Therefore you have to use the .is extension in import paths.
TypeScript cannot possibly know what URI you are going to use to serve your files, therefore it simply must trust that the module path you gave it is correct. In this case, you gave it a path to a URI that doesn't exist, but TypeScript cannot know that, so there is nothing it can do about it.
If you are serving the module with a URI that ends in .js, then your module path needs to end in .js. If your module path doesn't end in .js, then you need to serve it up at a URI that does not end in .js.
Note that the W3C strongly advises against using file extensions in URIs, because it makes it harder to evolve your system, and advocates to instead rely on Content Negotiation.
Rewriting paths would break a couple of fundamental design principles of TypeScript. One design principle is that TypeScript is a proper superset of ECMAScript and every valid ECMAScript program and module is a semantically equivalent TypeScript program and module. Rewriting paths would break that principle, because a piece of ECMAScript would behave differently depending on whether it is executed as ECMAScript or TypeScript. Imagine, you have the following code:
./hello
export default "ECMAScript";
./hello.js
export default "TypeScript";
./main
import Hello from "./hello";
console.log(Hello);
If TypeScript did what you suggest, this would print two different things depending on whether you execute it as ECMAScript or as TypeScript, but the TypeScript design principles say that TypeScript does never change the meaning of ECMAScript. When I execute a piece of ECMAScript as TypeScript, it should behave exactly as it does when I execute it as ECMAScript.
You can use the same solution as me
File: tsconfig.json
"compilerOptions": {
"module": "commonjs", ==> not required extension when import
"target": "ES6",
},
Because use commonjs, you must remove "type": "module" in package.json
Done :D
I'm not clear with the difference between importing a js module by name (like the common react or polymer lit-element usage) or by path (aka npm modules).
What's the difference and why there is a difference in the first place?
Why can't I import lit-element using:
import { LitElement, html } from 'lit-element'
like a normal npm module and use it in the browser without getting this error: Failed to resolve module specifier. Relative references must start with either "/", "./", or "../".?
Using ES6 module syntax in the browser is currently only supported with paths as specifiers because it's not yet clear how bare specifiers are going to get resolved to URLs.
Other specifiers are reserved for future-use, such as importing built-in modules.
The nodejs module resolution algorithm cannot be replicated because it requires testing for file existence, which is reasonable locally but not sensible over HTTP - and of course there's no node_modules folder anywhere.
Work is ongoing for allowing more elaborate solutions, e.g. module loaders or import maps. Paths were just the minimal viable solution that could be released to the public, while guaranteeing forward-compatibility.
Sources: [1], [2], [3]
I'm trying to use lodash in my web application. I have installed lodash using npm in my local project.
I plan on using the ES6 modules in my code.
Here is my main.js file:
import * as _ from "lodash";
_.each([1, 2, 3, 4], (i) => {
console.log('index each ' + i);
});
And I have included it in index.html as:
<script src="js/main.js", type="module"></script>
But I get the following error in the browser console.
Uncaught TypeError: Failed to resolve module specifier "lodash".
Relative references must start with either "/", "./", or "../".
Note: I do not wish to use any bundling tool.
If you don't wish to use any bundling tools, you will need to provide a path to the lodash folder within node_modules, relative to the JavaScript file that you have the import statement in.
If you do not wish to use a bundler, it would also be worthwhile importing from the specific file, the function you need. For example:
import _each from '../node_modules/lodash/each'
As of 2021, please consider the following statement by Márton Salomváry (Jan 2018):
Unfortunately even most libraries authored or published in ES6 module format will not work because they target transpilers and rely on the Node.js ecosystem. Why is that a problem? Using bare module paths like import _ from 'lodash' is currently invalid, browsers don’t know what to do with them.
And also the statement by Jake Archibald (May 2017):
"Bare" import specifiers aren't currently supported.
Valid module specifiers must match one of the following:
A full non-relative URL.
Starts with /.
Starts with ./.
Starts with ../.
And javascript.info:
In the browser, import must get either a relative or absolute URL. Modules without any path are called “bare” modules. Such modules are not allowed in import.
Certain environments, like Node.js or bundle tools allow bare modules, without any path, as they have their own ways for finding modules and hooks to fine-tune them. But browsers do not support bare modules yet.
Bundlers facilitate the use of "Bare Imports" which is not supported by the browser yet. Unless you bundle your code, I recommend using the solution proposed by #Asler. Besides, a lot of work is currently being done to study the implementation of "Bare Imports" in the browser, please follow this link if you want to monitor the overall progress.
Eventually you can't use JS modules on browser like that. These modules are for webpack or other bundler.
Try module lodash-es
import each from '../node_modules/lodash-es/each.js'
If you are trying to import css file, make sure to mention .css in import statement.
you can add your node_modules to the public dirs, so you can easily shorten your importing syntax from ../../../../node_modules/my-package into /my-package
also, you need to specify the full path including the file and the extension
import mod from "/my-package/file.mjs"
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.