I wrote an npm tool (similar to the one in the node_modules./bin directory).
The tool is written in ts. The import syntax is used to import modules into the code. So I wrote type: "module" in package. json.
However, in the . bin/cli. js file, the require syntax is used. Only type: "commonjs" can use the following code.
#!/ usr/bin/env node
require('../dist/index.js')
How should we deal with this situation?
Based on the Creating ESM-based shell scripts for Unix and Windows with Node.js blog post by Dr. Axel Rauschmayer, you should be able to rename your bin file from cli.js to cli.mjs:
#!/ usr/bin/env node
import '../dist/index.js';
I'd expect that nodejs should be then use the package.json "type": "module" value to treat your module as ESM after that.
You can then just run the bin file as usual:
$ ./bin/cli.mjs
Related
I wrote a few code that use plain browser Javascript APIs only and can be run well within browser HTML (served by IIS Server or Chrome Extensions). Now I want to contribute to the community by writing a library I have not seen on the market yet. However looking at current solutions, I am at loss at how a project is even built (WebPack/Browserify etc). A side note: I never actually work with NodeJS/NPM before.
For example I have this TypeScript project with the main file AwesomeClass.ts like this:
import { Helper1 } from "./Helper1.js";
import { Helper2 } from "./Helper2.js";
export class AwesomeClass {
doSomething() {
new Helper1().doSomething();
new Helper2().doSomething();
}
}
When built with tsc (I use VS Code as IDE), I can perfectly put this inside an Javascript module and browser can run it.
import { AwesomeClass } from "./AwesomeClass.js";
// Do something with AwesomeClass
So my question is, how do I build and distribute AwesomeClass? Maybe no NPM needed, but from a CDN? Ideally, I think somehow I should have the following output in a dist folder and developer can refer them either by hosting the files by themselves or use a CDN:
awesomeclass.js: For those who want to just use AwesomeClass without module feature (I think it's called UMD?). I.e. expose the AwesomeClass to global scope.
awesomeclass.es6.js: For those who want to use AwesomeClass by using import statement, like import { AwesomeClass } from "https://cdn.example.com/awesomeclass.es6.js";. I like this approach best and want to use this.
I should have something like awesomeclass.d.ts so those using TypeScript can use it. This one is especially tricky because so far I still don't understand how to make it work for 2nd scenario. TypeScript cannot get the type from an import statement from Javascript, and even ignoring that, I cannot get any typing for import statements.
In all cases, I would rather have only one js/ts file packed together if possible but not a deal breaker if I cannot (i.e. user will have to download Helper1.js and Helper2.js as well if I cannot).
Here's my current tsconfig.json:
{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": true,
"target": "ES2020",
"module": "ES2020",
"declaration": true
},
"exclude": [
"node_modules"
]
}
There's quite a lot of things you're asking and each of them have several answers so I'll try to provide you with a bit of an overview of your options.
Compiling everything to a single file
One of the things you asked is how you can compile everything to a single file. You can do that in 2 different ways, either using webpack to bundle it for client or using typescript directly.
If you use typescript you have to set outFile to a specific file, in which case it will compile everything to that file, however you can only do that if your module is also set to amd or system, both of which are not ideal. While this works it's something I'd suggest you don't use.
Instead you should use webpack to bundle all your stuff with the output option, in which case webpack will use ts-loader to invoke typescript for you, compile your stuff and bundle it into a single file.
You should also note here that this is only applicable if you actually want to serve it through web and not if you're building a library. If you're building an npm package that you're planning on letting people install with something like npx packageName so that you can use it like import somePackage from some-package, then you should be compiling your stuff to a /lib directory into normal javascript and just let them import it as javascript. There's no reason for why you should provide them with the original typescript in that case.
How to build and distribute it
It really depends on what exactly you're building and how it'll be used, however overall you have 2 main options.
You can either host it somewhere on some server with a domain of your choice so that people can download it. Or you can put it anywhere like a normal git repo where people can download it. In this case you'll have to compile it with webpack yourself, upload it yourself and then just share the link with people i.e. https://example.com/downloads/awesome. Alternatively you can use webpack to render it server side and expose an API to people that they can call in order to get your code, then it will deliver the bundled javascript to them once they call the API i.e. https://api.example.com/awesome which will hit your API with a GET request, which will route to awesome and then you invoke webpack's compiler to bundle your code server side.
Your other option is to build your package like normal, compile it and then use the official npm registry to host your npm package. Using this option will allow people to npx package or npm i package on your code and also allow them to use it like import awesome from 'awesome'. If you go this route then using webpack isn't necessary, or it depends, because people using it will import it into their own project and build it into their own webpack setup and bundle if required. In this case all you have to do is compile your typescript to something like a /lib and allow them to install and import it.
From the things that you're asking/saying it seems to me that you're trying to create an npm package, for that all you need is to create the package, compile your typescript, set up an account on npm and push your package to their registry, from where you can let anyone install it. For this you also shouldn't care at all about compiling all your code to a single file because it doesn't matter, if they use import awesome from 'awesome' then that file can again import anything else inside your own package and they wouldn't know it. You can just tsc your code to an output directory and let them know which is the default export for that package.
If your code has to run in browser then I don't believe just using typescript will be enough, in that case you'll have to use webpack, you might also need babel if you need to support older browsers and polyfills, which is something you can add to webpack, then you'll use webpack to compile your bundle. Webpack will then invoke typescript, through ts-loader, which will compile and bundle your code for you ready for web. In this case you'll still need to push this code to the npm registry as a package so others can use it.
The choice between those options is entirely dependent on what it is and who's going to use it and how.
This should be the core requirements in the tscongfig.json file
{
"compilerOptions": {
"lib": ["es6", "es2020.promise", "dom", "es2020"],
"declaration": true,
"target": "es2020",
"module": "es2020"
}
}
While to pack everything in just one file, I think you have to use something like webpack; but I don't know enough this topic to help you on that, sorry.
I am creating an app with Electron and Vue (using js not ts).
When I run the app using npm run electron:serve the app runs fine.
I now want to build a Windows exe so I can distribute my app. I have tried using electron-builder, electron-packager and electron-forge. Whenever I can get the build to finish, running the exe throws the cannot use import statement outside a module error (referring to the first import statement it finds, i.e. import { app, protocol, BrowserWindow } from 'electron').
I've tried adding "type":"module" to my package.json but (due a bug in Vue, according to this question), that throws Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]
I've also tried changing all my import statements to require but this doesn't work because some of the node modules I'm using use import and the error just throws for those instead.
I'm tearing my hair out over this. Where do I go from here?
UPDATE:
I have found a workaround for the Vue bug and posted my findings on the linked question. I can now add "type":"module" to my package.json.
However, I now get an error thrown when I run npm run electron:serve and from my built exe:
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: <my_project_root>\dist_electron\index.js
require() of ES modules is not supported.
require() of <my_project_root>\dist_electron\index.js from <my_project_root>\node_modules\electron\dist\resources\default_app.asar\main.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
To be clear, I'm not using require in any of my source code, but the compiled(?) version does?
What's going on here?
UPDATE 2:
As requested, here is a minimal reproducible example that maintains original folder structure, configs and package.json
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
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 really new to Typescript2. I'm loving it and have written a module that I can easily import and use in other Typescript2 projects. But I also want my library to be able to be used as a standalone by simply using an HTML tag. When I do that, though, the browser complains "exports is undefined."
I'm guessing I have to write some sort of javascript that imports my module and instantiates it, and then have my browser load that wrapper script. Am I on the right track? How do I create a typescript that transpiles into something that I can just load natively in the browser with a script tag that just loads and instantiates?
You are on the right track. What you need is a module loader. For the browser, requirejs is mentioned in the typescript documentation, although there are several options (requirejs, browserify, webpack, rollup).
I use requirejs with typescript, so I have experience doing the following steps:
For requirejs, you could do the following to get started.
Ensure that you are compiling to modules in the AMD format when running the typescript compiler command (tsc) by using the typescript configuration file or command-line compiler option arguments
// tsconfig.json
{
"compilerOptions": {
"module": "amd"
}
}
Get requirejs
One source is http://requirejs.org/docs/download.html
Note the name of the .js file that is output by the typescript compiler
For example "my-script.js"
Add a script tag to the page
For example <script data-main="scripts/my-script" src="require.js"></script>
There is a simple "getting started" page for requirejs http://requirejs.org/docs/start.html
As a bonus, to export multiple modules to a single file, you can use the --outFile compiler option for typescript.