I have a web project that uses TypeScript and I’m able to easily import, see and use npm modules, but when I load the web page in the browser the npm modules are not found.
My VS Code project is set up like so:
/mypage.html
/myproject.ts (my typescript)
/myproject.js (javascript created by typescript)
/tsconfig.json
/.vscode/tasks.json
If I reference a library in my TS file and view it in the browser it can’t find that module or gives other errors. See below.
But if I manually go into the npm module and dig around and find the library.js
file and then copy that to a folder in the root directory then the web page in the browser can see and use that library. This works but by manually copying the library into the main source directory I’m losing many of the benefits of npm.
I believe if I can define an export directory for all the web page files I can have my TypeScript files copied to it, my HTML pages copied to it and npm modules copied to it, then I believe it will work when run in the browser. It seems it should work this way?
Is there a way to do this? Is there a way to do this with tsconfig.json
or jsconfig.json
? Is there a webconfig.json
or vscodeconfig.json
that will copy all files in a directory to another directory (including referenced npm modules)?
I have a Node.js Express project that uses TypeScript and any imported libraries just work. I don’t have to do anything to get it to work (tsconfig.json
maybe). I want that same thing for my client side TypeScript that compiles to JavaScript used in web pages project.
I feel like this should be basic 101 web development (and it works to a point) but I can’t seem to find a way to get it to integrate the npm modules in the output.
Note: Suggestions have been pointing to using bundler. I’ve started down this path but still learning. See below.
More info:
I’m using a local server (http://localhost) to load the web page (there are plugins for vscode) in the browser. I have a separate project that is a nodejs project that starts a nodejs server, also running localhost.
TS to JS is converted by settings in the tsconfig file. I have file watcher on so that saving typescript files creates the js files.
I’m not using webpack or any other bundler and many have been suggested. I’m not against it only I haven’t found a guide yet and some are confusing. Most guides I found are how to setup a React or Next js project. I want to setup a vanilla js project. A few projects like Snowpack or Vite look like what I want but I can’t tell if it’s a plugin for vscode or a vscode replacement (it looks like a replacement). Some of them seem to run in the browser.
I’ve also just read about import maps
. This looks helpful because it can point to the npm modules on a remote site if they exist already. But I don’t know how it will work locally. I will try to see if I can get import maps working.
I want to use this module in my web projects. Specifically BaseClass.ts
.
Web Project Example
My basic web project that that is related to this question is here. It is all setup and working except for the issue mentioned in this post (the module is not rolled up, bundled or exported or included when typescript is compiled or when the page is viewed in the browser).
I just updated the npm package (1.0.2) to include Javascript files. I had only had Typescript files before. Not sure if it helps.
.vscode/tasks:
{
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": [
"$tsc-watch"
],
"group": {
"kind": "build",
"isDefault": true
},
"label": "tsc: watch - tsconfig.json"
}
]
}
tsconfig.json (suggestions welcome!)
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"inlineSourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"allowUnreachableCode": true,
"skipLibCheck": true
}
}
With the tsconfig settings I have above I get errors and the library is not found.
script.js:2 Uncaught ReferenceError: exports is not defined
at script.js:2:23
The source tab shows only the script.js file is included (the npm module is not found or included):
HTML: (simplified)
<script type="module">
import {BaseClass} from "base-class-ts"
export class MyClass extends BaseClass {
constructor() {
super();
console.log("Hello world")
}
}
BaseClass.startWhenReady(MyClass);
</script>
If you are working on an answer for the bounty and using a bundler, please add how to support source maps (if possible).
This is already a long post but I’m putting some things I find here (rather than in a comment or an answer) and no I haven’t tried them all I’m still figuring this out too:
- Using import maps to access the node module:
https://youtube.com/watch?v=En1lSiMjLvU
2
Answers
TypeScript can compile your code to a variety of formats, but usually it will either be ECMAScript Module format (ESM) or the CommonJS format (CJS).
ESM looks like
import foo from 'foo'
while CJS looks likeconst foo = require('foo')
.When you import or require ‘foo’, there is a resolution step to decide where to look for the module foo. Import resolution for code running in node.js is different to resolution in the browser. For this reason code written in the node.js style typically does not work in the browser as-is. NPM is a package manager for Node, so this system should be expected to work in Node but not in the browser.
Modern browsers support ESM modules, although the import resolution will differ, but they don’t support CJS modules. We can therefore make some progress by ensuring that we compile to ESM modules with TypeScript. It is possible then to translate the imports to the correct locations with an import map – the problem here is that an import mapping is required for every incompatible import reference, and it can be difficult to identify all of those, and even then they are likely to change as you develop.
For a production application is has been popular to transpile code into older more widely supported JavaScript, and use a bundler to combine the code into 1 or more large bundles, that use more primitive JavaScript features to emulate modules, without relying on any module format. It’s debatable whether this is still necessary – TypeScript alone can handle transpiling to early forms of JavaScript, and caniuse.com says that ESM modules are supported by 97% of browsers in use, so it is possible today to use ESM modules for a final application, which could be exactly the same as the development setup.
Ultimately, and perhaps you don’t want to hear this, but converting from the NPM/node_modules format to a browser suitable format is a non-trivial task, and its best done by an existing build process. Vite is probably the fastest way to get started with a vanilla JavaScript project that can you npm modules (Get Started with Vite). Vite is a development server and build tool that runs from the command line. They provide a command line tool to create an empty project that is setup to use Vite.
If you want this to be any simpler, then the answer is not to use npm, but instead to use a system that is web compatible. For example, CDNs provide links that you can insert straight into a web page. There may be a package manager out that there that works like npm, but designed for the browser exclusively, but if so I don’t know it – to be frank in the JavaScript world often we have really become quite dependant on build tools, that can sometimes be quite complex, so the quick answer to doing things the conventional way is to use the existing tools or frameworks rather than try to recreate it.
You seem to want to use NPM to manage TS packages in the browser.
So you are fighting at least two uphill battles, especially if one of your goals is to avoid JS bundlers (one of your best allies in this battle). While it is possible and similar things have been accomplished before, you are trying to use and combine tools in ways that were not originally intended.
Also note even if you get your package working as a JS module from NPM, a lot of TS information is stripped when compiling to JS. So there may be limitations with importing a base class and extending it like in your script.js.
Having said that, I will try to offer some specific advice:
First, I suggest getting the package you wish to import working in a Node environment;
base-class-ts
does not seem to be a valid NPM package.Yes, it has been published to NPM and can be
npm install
‘ed. But that does not mean it will work. If your package doesn’t work, you can’t expect your app that depends on this package to work, either.NPM does not understand TS. So you cannot point
main:
to ats
file in yourpackage.json
. (This may be possible when using a bundler that understands TS files, but by default NPM does not understand TS files.)The solution may be as simple as pointing
main
to your compiledjs
file (which you have already included in the package).Because
base-class-ts
is not a valid NPM module, the minimal Node project using this package results in an error. I tried multiple versions of the minimal NodeJS script:The following also result in
ERR_MODULE_NOT_FOUND
errors. There is simply no way to use this NPM package as-is.