Typescript2 path module resolution - node.js

tl;dr : module resolution does not apply ?
Hello,
I am playing around with Typescript2 module resolution feature.
I've noticed that it is now possible to specify "Paths", so that you can do the following :
Old way
import {a} from "../../../foo"
New way
import {a} from "services/foo"
To do so, you need to add some configs to your tsconfig.json
"compilerOptions": {
"baseUrl": ".",
"paths": {
"services/*": ["./application/core/services/*"],
}
}
Problem that I have, is that when compiled, the import actually doesn't change. My javascript output still contains that import from "services/foo", so that obviously crash at runtime on my node server.
I use gulp-typescript to compile my javascript files :
var tsProject = ts.createProject("tsconfig.json");
return tsProject.src()
.pipe(sourcemaps.init())
.pipe(tsProject()).js
.pipe(sourcemaps.write("../api"))
.pipe(gulp.dest(function(file) {
return file.base;
}));
I am completely lost here and would love to use that module resolution, so that I can move away from that ../../ hell of imports. Any help would be more than appreciated !

The problem here is that the JavaScript Engine knows nothing about your TypeScript config, what you specify in your tsconfig is only used "compile time", when you have compiled your TypeScript into JS you need to do the same job as the TS compiler did but to save the resolved path in the JS file.
Simply put, all JS files needs to be processed and the aliases replaced with "real" paths.
Tip: Use the npm tool tspath (https://www.npmjs.com/package/tspath), it requires 0 configuration, just run it in somewhere in your project and all JS files will be processed and ready to run!

Related

Persistent undefined error in typescript import export

There's already a LOT of questions about typescript in multiple files.. for instance, this one,
Typescript import/export
Interesting question and answer, I simplified and tested it, see below.. but whatever I try, I still get
Uncaught TypeError: Cannot read properties of undefined (reading 'A')
.. as does any other example of import/export in TypeScript I found online. Whatever I do, whatever object I try export (class, function, const) with or without using a module: I get the same error.
Maybe there is something wrong in my NPM/TSC/React configuration ? Should I change e.g. tsconfig.js when i want to use more than one typescript file in a project ? I'm lost, what do I miss ?
tsconfig.json
{ // TypeScript configuration file: provides options to the TypeScript
// compiler (tsc) and makes VSCode recognize this folder as a TS project,
// enabling the VSCode build tasks "tsc: build" and "tsc: watch".
"compilerOptions": {
"target": "es5", // Compatible with older browsers
"module": "umd", // Compatible with both Node.js and browser
"moduleResolution": "node", // Tell tsc to look in node_modules for modules
"sourceMap": true, // Creates *.js.map files
"jsx": "react", // Causes inline XML (JSX code) to be expanded
"strict": true, // Strict types, eg. prohibits `var x=0; x=null`
"alwaysStrict": true // Enable JavaScript's "use strict" mode
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
first.tsx
const A ={
val: 'A'
}
export { A }
app.tsx
import { A } from "./first";
// ... other code
function reportPerson()
{
console.log(A);
}
.. Both files translate to .js with TSC, but A is reported by the Google Chrome console as undefined,
Both tsx files are in the same directory, TSC converts them both to JS without any issue.
What's going on ?
Thanks everyone for the advice (I didn't solve the above minimal example either..)
In order to properly link my stuff together, I've now put Parcel 2 to work,
https://www.npmjs.com/package/parcel
npm i parcel
This is basically a bundler, that allows separate ts files to be concatenated after they are compiled to Javascript and it will put everything in a \dist directory,
parcel build src/index.html
Based on a small react example, I put my first "modulized" little app in TypeScript to work. Then, with the help of expert advise, I proceeded with twgl.js, which is a great toolkit for Webgl2.
npm install twgl.js
This javascript library even has sub-modules.. and everything links fine now, I can access (all of?) twgl with
import * as twgl from "./twgl-full.js";

How to use TypeScript import statement instead of <reference path...> in a Web application (ASP.NET Core)?

Context
I have (had) a working version typescript Hello World in my Web application (ASP.NET Core)
Using typscript compiler via NuGet package "Microsoft.TypeScript.MSBuild" Version="4.4.2" and tsconfig.json. (see below)
I've wanted to use a 3rd party lib, and successfully added "#types/lodash": "^4.14.175" via packages.json (see below)
I've added /// <reference path="../node_modules/#types/lodash/index.d.ts"/> (see below)
All works, but the line /// <reference path="..." is underlined green and ESLint says
Do not use triple slash reference for index.d.ts, use import instead.
OK, I am going to use export/import later anyway, so I've edited the triple slash reference line to be a comment, and added the line import * as _ from "lodash" which compiles fine, but when running in chrome causes runtime error:
Cannot use import statement outside a module
so I changed my <script tag to the following: <script type="module" src="~/js/app.js"></script>
However this causes the following chrome runtime error:
Failed to resolve module specifier "lodash". Relative references must start with either "/", "./", or "../".
Question
Now I am stuck, and with my very limited knowledge somehow I guess some step/transformation is missing, but what? I've tried to include some path in my .ts file's import statement (causing compile errors). Compile time I would like to use the working import referring to the #typings, but runtime the lodash.js is coming from cdn, the two nothing to do with each other...
app.ts
// commented out / <reference path="../node_modules/#types/lodash/index.d.ts"/>
import * as _ from "lodash"
console.log(_.camelCase("Hello"));
emitted app.js
// commented out / <reference path="../node_modules/#types/lodash/index.d.ts"/>
import * as _ from "lodash";
console.log(_.camelCase("Hello"));
//# sourceMappingURL=app.js.map
index.html
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>
<script type="module" src="~/js/app.js"></script>
tsconfig.json
{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "es6",
"module": "ES6",
"outDir": "wwwroot/js"
},
"exclude": [
"node_modules",
"wwwroot"
]
}
packages.json
{
"version": "1.0.0",
"name": "asp.net",
"private": true,
"devDependencies": {
"#types/lodash": "^4.14.175"
}
}
Try to modify the tsconfig.json
// tsconfig.json
{
...
#types: ["node_modules/"] // or typings
}
Or use ES5 require
const _ = require("lodash");
Found the following possible solutions
The required files need to be copied over to wwwroot folder, where they can be accessed when the application runs.
For this you'd need either use the bundler to bundle the files together (should be in default ASP.NET Core project template) or use task runners such as Gulp or Grunt to run tasks on build/publishing, which does that for you. See ASP.NET Core Docs on Gulp examples.
Original answer: https://stackoverflow.com/a/43513137/13747848
Note: Please give credit to original respondent!
Edit
For the error
Uncaught TypeError: Failed to resolve module specifier "lodash". Relative references must start with either "/", "./", or "../".
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.
Original answer: https://stackoverflow.com/a/66484496/13747848
Note: Please give credit to original respondent!
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'
Original answer: https://stackoverflow.com/a/52558858/13747848
Note: Please give credit to original respondent!

How to import a node module inside an angular web worker?

I try to import a node module inside an Angular 8 web worker, but get an compile error 'Cannot find module'. Anyone know how to solve this?
I created a new worker inside my electron project with ng generate web-worker app, like described in the above mentioned ng documentation.
All works fine until i add some import like path or fs-extra e.g.:
/// <reference lib="webworker" />
import * as path from 'path';
addEventListener('message', ({ data }) => {
console.log(path.resolve('/'))
const response = `worker response to ${data}`;
postMessage(response);
});
This import works fine in any other ts component but inside the web worker i get a compile error with this message e.g.
Error: app/app.worker.ts:3:23 - error TS2307: Cannot find module 'path'.
How can i fix this? Maybe i need some additional parameter in the generated tsconfig.worker.json?
To reproduce the error, run:
$ git clone https://github.com/hoefling/stackoverflow-57774039
$ cd stackoverflow-57774039
$ yarn build
Or check out the project's build log on Travis.
Note:
1) I only found this as a similar problem, but the answer handles only custom modules.
2) I tested the same import with a minimal electron seed which uses web workers and it worked, but this example uses plain java script without angular.
1. TypeScript error
As you've noticed the first error is a TypeScript error. Looking at the tsconfig.worker.json I've found that it sets types to an empty array:
{
"compilerOptions": {
"types": [],
// ...
}
// ...
}
Specifying types turns off the automatic inclusion of #types packages. Which is a problem in this case because path has its type definitions in #types/node.
So let's fix that by explicitly adding node to the types array:
{
"compilerOptions": {
"types": [
"node"
],
// ...
}
// ...
}
This fixes the TypeScript error, however trying to build again we're greeted with a very similar error. This time from Webpack directly.
2. Webpack error
ERROR in ./src/app/app.worker.ts (./node_modules/worker-plugin/dist/loader.js!./src/app/app.worker.ts)
Module build failed (from ./node_modules/worker-plugin/dist/loader.js):
ModuleNotFoundError: Module not found: Error: Can't resolve 'path' in './src/app'
To figure this one out we need to dig quite a lot deeper...
Why it works everywhere else
First it's important to understand why importing path works in all the other modules. Webpack has the concept of targets (web, node, etc). Webpack uses this target to decide which default options and plugins to use.
Ordinarily the target of a Angular application using #angular-devkit/build-angular:browser would be web. However in your case, the postinstall:electron script actually patches node_modules to change that:
postinstall.js (parts omitted for brevity)
const f_angular = 'node_modules/#angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js';
fs.readFile(f_angular, 'utf8', function (err, data) {
var result = data.replace(/target: "electron-renderer",/g, '');
var result = result.replace(/target: "web",/g, '');
var result = result.replace(/return \{/g, 'return {target: "electron-renderer",');
fs.writeFile(f_angular, result, 'utf8');
});
The target electron-renderer is treated by Webpack similarily to node. Especially interesting for us: It adds the NodeTargetPlugin by default.
What does that plugin do, you wonder? It adds all known built in Node.js modules as externals. When building the application, Webpack will not attempt to bundle externals. Instead they are resolved using require at runtime. This is what makes importing path work, even though it's not installed as a module known to Webpack.
Why it doesn't work for the worker
The worker is compiled separately using the WorkerPlugin. In their documentation they state:
By default, WorkerPlugin doesn't run any of your configured Webpack plugins when bundling worker code - this avoids running things like html-webpack-plugin twice. For cases where it's necessary to apply a plugin to Worker code, use the plugins option.
Looking at the usage of WorkerPlugin deep within #angular-devkit we see the following:
#angular-devkit/src/angular-cli-files/models/webpack-configs/worker.js (simplified)
new WorkerPlugin({
globalObject: false,
plugins: [
getTypescriptWorkerPlugin(wco, workerTsConfigPath)
],
})
As we can see it uses the plugins option, but only for a single plugin which is responsible for the TypeScript compilation. This way the default plugins, configured by Webpack, including NodeTargetPlugin get lost and are not used for the worker.
Solution
To fix this we have to modify the Webpack config. And to do that we'll use #angular-builders/custom-webpack. Go ahead and install that package.
Next, open angular.json and update projects > angular-electron > architect > build:
"build": {
"builder": "#angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./extra-webpack.config.js"
}
// existing options
}
}
Repeat the same for serve.
Now, create extra-webpack.config.js in the same directory as angular.json:
const WorkerPlugin = require('worker-plugin');
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
module.exports = (config, options) => {
let workerPlugin = config.plugins.find(p => p instanceof WorkerPlugin);
if (workerPlugin) {
workerPlugin.options.plugins.push(new NodeTargetPlugin());
}
return config;
};
The file exports a function which will be called by #angular-builders/custom-webpack with the existing Webpack config object. We can then search all plugins for an instance of the WorkerPlugin and patch its options adding the NodeTargetPlugin.

Equivalent for __dirname in TypeScript when using target/module=esnext

I need to calculate a pathname relative to the file-system location of the module. I am using the latest TypeScript on Node.js 12.x. For other reasons in tsconfig.json I have set
"target": "esnext",
"module": "esnext",
This triggers a mode that is strict to the Node.js support for ES6 Modules. In that mode the __dirname variable is not available because that global is not defined in the ESanything spec. What we're supposed to do instead is access the import.meta.url variable and extract the directory name.
For an example see the last answer on this question: Alternative for __dirname in node when using the --experimental-modules flag
But in the TypeScript DefinitelyTyped collection the ImportMeta class is not defined to include a url member. Therefore the code fails to compile:
tdn.ts:7:25 - error TS2339: Property 'url' does not exist on type 'ImportMeta'.
7 console.log(import.meta.url);
~~~
I wasn't able to find the definition of ImportMeta in the Definitely Typed repository. But it seems to be improperly defined.
UPDATE: In node_modules//typescript/lib/lib.es5.d.ts I found this:
/**
* The type of `import.meta`.
*
* If you need to declare that a given property exists on `import.meta`,
* this type may be augmented via interface merging.
*/
interface ImportMeta {
}
Ugh...
/UPDATE
In the Node.js 12.x documentation on the ES Modules page it clearly describes the shape of import.meta and that we're supposed to do something like:
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
'__dirname', '__filename' and 'require'...etc are NodeJS specific keywords, and by default typescript doesn't recognize them, although you need to know that the compiler compile the ts file to js file (by default) and it works fine, in order to clear the errors you can to run this on your terminal (or cmd on windows):
npm install --save #types/node
that would install nodejs types definitions and allow you to avoid this type of errors when writing your nodejs programs in typescript.
using import.meta and fileToURLupdate this works...
As per NODE.org, both CommonJs variables specially __dirname or __filename are not available in ES Modules.
We have to replicate those commonJs variables from via import.meta.url.
Source: https://nodejs.org/api/esm.html#esm_no_filename_or_dirname

How to not bundle node_modules, but use them normally in node.js?

Architecture
I would like to share code between client and server side. I have defined aliases in the webpack config:
resolve: {
// Absolute paths: https://github.com/webpack/webpack/issues/109
alias: {
server : absPath('/src/server/'),
app : absPath('/src/app/'),
client : absPath('/src/client/'),
}
},
Problem
Now on the server side I need to include webpack in order to recognize the correct paths when I require a file. For example
require('app/somefile.js')
will fail in pure node.js because can't find the app folder.
What I need (read the What I need updated section)
I need to be able to use the webpack aliases. I was thinking about making a bundle of all the server part without any file from node_modules. In this way when the server starts it will use node_modules from the node_modules folder instead of a minified js file (Why? 1st: it doesn't work. 2nd: is bad, because node_modules are compiled based on platform. So I don't want my win files to go on a unix server).
Output:
Compiled server.js file without any node_modules included.
Let the server.js to use node_modules;
What I need updated
As I've noticed in https://github.com/webpack/webpack/issues/135 making a bundled server.js will mess up with all the io operation file paths.
A better idea would be to leave node.js server files as they are, but replace the require method provided with a custom webpack require which takes in account configurations such as aliases (others?)... Can be done how require.js has done to run on node.js server.
What I've tried
By adding this plugin in webpack
new webpack.optimize.CommonsChunkPlugin(/* chunkName= */"ignore", /* filename= */"server.bundle.js")
Entries:
entry: {
client: "./src/client/index.js",
server: "./src/server/index.js",
ignore: ['the_only_node_module'] // But I need to do that for every node_module
},
It will create a file server.js which only contains my server code. Then creates a server.bundle.js which is not used. But the problem is that webpack includes the webpackJsonp function in the server.bundle.js file. Therefore both the client and server will not work.
It should be a way to just disable node_modules on one entry.
What I've tried # 2
I've managed to exclude the path, but requires doesn't work because are already minified. So the source looks like require(3) instead of require('my-module'). Each require string has been converted to an integer so it doesn't work.
In order to work I also need to patch the require function that webpack exports to add the node.js native require function (this is easy manually, but should be done automatically).
What I've tried # 3
In the webpack configuration:
{target: "node"}
This only adds an exports variable (not sure about what else it does because I've diffed the output).
What I've tried # 4 (almost there)
Using
require.ensure('my_module')
and then replacing all occurrences of r(2).ensure with require. I don't know if the r(2) part is always the same and because of this might not be automated.
Solved
Thanks to ColCh for enlighten me on how to do here.
require = require('enhanced-require')(module, require('../../webpack.config'));
By changing the require method in node.js it will make node.js to pass all requires trough the webpack require function which allow us to use aliases and other gifts! Thanks ColCh!
Related
https://www.bountysource.com/issues/1660629-what-s-the-right-way-to-use-webpack-specific-functionality-in-node-js
https://github.com/webpack/webpack/issues/135
http://webpack.github.io/docs/configuration.html#target
https://github.com/webpack/webpack/issues/458
How to simultaneously create both 'web' and 'node' versions of a bundle with Webpack?
http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/
Thanks
Thanks to ColCh for enlighten me on how to do here.
require = require('enhanced-require')(module, require('../../webpack.config'));
By changing the require method in node.js it will make node.js to pass all requires trough the webpack require function which allow us to use aliases and other gifts! Thanks ColCh!
My solution was:
{
// make sure that webpack will externalize
// modules using Node's module API (CommonJS 2)
output: { ...output, libraryTarget: 'commonjs2' },
// externalize all require() calls to non-relative modules.
// Unless you do something funky, every time you import a module
// from node_modules, it should match the regex below
externals: /^[a-z0-9-]/,
// Optional: use this if you want to be able to require() the
// server bundles from Node.js later
target: 'node'
}

Resources