In a JavaScript project, a file titled index.js can be imported as such:
import SomeComponent from 'components/some-component'
Rather than having to specify index.js.
In my project, I prefer to use a different naming convention: some-component.component.js. This way I can tell what the file is from a glance (rather than having a million index.js).
What I'm trying to achieve is having this same import pattern happen for files with the pattern *.component.js. In other words:
import SomeComponent from 'components/some-component'
Rather than
import SomeComponent from 'components/some-component/some-component.component.js'
I have the following (abbreviated) jsconfig.json:
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"*": ["./*"]
}
}
}
Any ideas on how this could be achieved?
Thanks in advance!
Short answer: you can't customize the import resolution using a function (or RegExp). For a web project, you may use a tool like Webpack with a custom loader. But, that is a Webpack specific solution that will not work with other tools (like doing Ctrl-click in VSCode, or using TSC directly).
Long answer:
The resolution of the JavaScript files depends on the tools you use. The standards changed a little bit over the years, and while things are converging to ESM there are still a lot of inconsistencies.
The TypeScript module resolution is described here: https://www.typescriptlang.org/docs/handbook/module-resolution.html
You can have a baseUrl, merge multiple directories as one (rootDirs), or have a limited wildcard path mapping (https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping). You already used it in your question, so probably I'm not telling you anything new. But, that's where the TS configuration options end. (you can customize the compilation module resolution using the TSC API, but not from the CLI/tools config options)
NodeJS can hook a resolution mechanism for modules. For example, Yarn 2+ does that to support a feature called PNP (https://yarnpkg.com/features/pnp). However, TypeScript still needs to resolve types and it will not use your custom loading mechanism for that.
The situation with other tools like Webpack is similar.
The module resolution of Node and TypeScript are specific to those tools. The ESM standard is even more strict: the from part must be an absolute or relative URL, or a "bare identifier" that you need to map using an import map. In other words, you can't have a custom algorithm to resolve paths. The justification for that is described in the Import Maps standard repo: https://github.com/WICG/import-maps#a-programmable-resolution-hook
Conclusion: if you really-really want to do a custom module resolution, you may be able to create your own CLI using the TSC API. That may not work with other tools, and is probably easier to get used to another naming convention.
Related
I am attempting to use the memory adapter for PouchDB. I want to bundle my application (along with dependencies like Pouch and this adapter) using Rollup. In order to reduce this to a minimally reproducible issue imagine this is my application I want to bundle:
import PouchDB from 'pouchdb-browser'
import MemoryAdapterPlugin from 'pouchdb-adapter-memory'
PouchDB.plugin(MemoryAdapterPlugin)
Since I am using a node module I'm obviously going to need the rollup-node-resolve Rollup plugin. Also many of PouchDB's dependent modules are in CJS format so I'm going to also need the rollup-node-commonjs Rollup plugin. Finally PouchDB makes use of some built-in Node modules (such as events) and the memory adapter even more (buffer, etc). I'm not sure these are used at runtime (it may be just in PouchDB's code because it can work in Node or the browser) but to prevent a bundling error I'm going to also include the rollup-plugin-polyfill-node plugin but direct the resolve plugin to prefer built-ins to the browser target if they are needed and available.
With all that in place here is my rollup config:
import commonjs from '#rollup/plugin-commonjs'
import resolve from '#rollup/plugin-node-resolve'
import polyfillNode from 'rollup-plugin-polyfill-node'
export default {
input: 'index.js',
output: {
format: 'iife',
name: 'app',
file: 'bundle.js'
},
plugins: [
commonjs({
requireReturnsDefault: "auto",
}),
polyfillNode(),
resolve({
preferBuiltins: true,
browser: true
}),
]
}
This will bundle. But when I load up the bundle in a browser I get an error about the inherits polyfill not working at this line:
https://github.com/FredKSchott/rollup-plugin-polyfill-node/blob/main/polyfills/inherits.js#L7
It says superCtor is undefined. If I step back a level on the backtrace it comes from this line:
https://github.com/nodejs/readable-stream/blob/main/lib/_stream_duplex.js#L46
The Readable is undefined. When I bundle I get warnings about circular references. I think it fundementally revolves around the fact that some of the PouchDB dependencies use NPM packages as polyfills explicitly (like inherits and readable-stream) while the polyfillNode plugin is also providing some of those same polyfills and they are doing so in an incompatible way. But I don't know how to untangle it.
Finally was able to resolve this so going to provide the answer in case someone else needs to bundle PouchDB memory adapter in Rollup.
The readable streams polyfill is a mess of circular dependencies. This is fine under a CJS format but since Rollup converts CJS to ES format it becomes problematic as Rollup can't figure out the proper order to put things in. This leads to the use of inherits where the superclass is not yet defined. The readable streams polyfill has an open ticket regarding this. The general solution is to fix it upstream in Node and then cut a new copy of the polyfill based on that upstream fix. The upstream fix was made but the polyfill hasn't yet been updated. Once it is this should naturally resolve itself.
In the meantime we can use someone else's fork of the polyfill that has the circular dependencies resolved. This fork is mentioned in that issue but the punchline is to add this to the package.json:
"readable-stream": "npm:vite-compatible-readable-stream#^3.6.0",
This will force readable-stream to resolve to that fork. But that's not the whole story. There are some dependencies of the memory adapter that are locked to a different version of readable-stream. To resolve that we want to add the following to our rollup's resolve plugin:
dedupe: ['readable-stream']
This will force the readable-stream substitute that we have explicitly added to our project to be used anytime a readable stream is needed.
The final issue is that PouchDB is using an ancient version of memdown that also has circular dependency issues. The latest version does not and seems to work with the memory adapter just fine. There is an open ticket at PouchDB to update memdown and this will naturally resolve when that happens.
In the meantime to resolve that we are going follow a similar pattern as above. We will explicitly require memdown into our project although no need for a fork. Just a newer version. Then to force the memory adapter to use this version we add memdown to that dedup option as well.
I have been trying to build a library that requires its consumers to use a specific strategy per target. My current architecture follows:
[Application] -> contains -> [player] -> contains -> [renderer]
Currently, Renderer is an interface which needs to be replaced for different platforms:
Mobile -> uses MobileRenderer
Web -> uses WebRenderer
I have the freedom to use any bundlers - currently using Webpack for the app and Rollup for the lib - and what I could achieve follows:
My lib exports a Player interface and a createPlayer function which returns a PlayerInterface. Then, on the application side I have a Webpack alias that resolves the correct platform library based on a build input.
e.g:
import { createPlayer } from "my-lib";
const player = createPlayer()
Then we build the application with
npm run build --platform=web
to which webpack will transform the my-lib import into my-lib/platforms/web, which also contains an exported createPlayer function which uses the correct renderer.
My question is the, from the application's point of view, how can we make sure that we import the correct renderer per platform on build time while allowing tree-shaking (so only including the correct sources)? I find that using the build system to do that is quite obscure, as it doesn't leave a clear trace of what's going on.
Is there a better way of doing this?
Best,
You have a couple options. I'd recommend against having a compile-time switch, since that requires you to distribute multiple copies of your library (which isn't idiomatic). However, if you really wanted to do this, I'd suggest using webpack's ProvidePlugin or resolve.alias config field to dynamically link your code to the appropriate renderer at build time.
A more pragmatic approach, in my opinion, would be to have two entry points to your application, and allow the implementor to choose which entry point to use. This is similar to how react-dom switches between browser rendering and server rendering (i.e., react-dom versus react-dom/server):
// index.js
export * from './shared';
export {default as renderer} from './renderers/web';
// mobile/index.js
export * from '../shared';
export {default as renderer} from '../renderers/mobile';
Then, someone using your library could import {renderer, foo} from 'your-lib' or import {renderer, foo} from 'your-lib/mobile'.
Both of the above approaches work at build time.
While the latter approach normally forces you to choose which version of the library to use in your code, you can use webpack's resolve.alias configuration field to force imports to your-lib be redirected to your-lib/mobile.
The documentation I've read is rather hand-wavy about what exactly import does in Javascript, particularly in the Angular framework. I get that it imports modules from another file that has one or more exports. But there are many permutations of its syntax, and not all are discussed with much detail. I'm currently having a very hard time with the #asymmetik/ngx-leaflet-markercluster module. When I try to compile my Angular app, I get a message reading "Can't resolve 'leaflet.markercluster' in 'C:\sca_root\city8\node_modules#asymmetrik\ngx-leaflet-markercluster\dist\leaflet-markercluster" -- this is in reference to a line that reads simply
import 'leaflet.markercluster';
This seems to me (and I know, perhaps I am making too many assumptions here) that there should be a file in that same directory named leaflet.markercluster.js or perhaps leaflet.markercluster.ts (it's Javascript, not TypeScript, so it will be the former). But there is no file named leaflet.markercluster.js in that directory. These are the files in that directory:
leaflet-markercluster.directive.js.map
leaflet-markercluster.directive.metadata.json
leaflet-markercluster.module.d.ts
leaflet-markercluster.module.js
leaflet-markercluster.module.js.map
leaflet-markercluster.module.metadata.json
leaflet-markercluster.directive.d.ts
eaflet-markercluster.directive.js
Which one would that import statement import? If not any of them, where outside this directory would it import that file from? What other information (perhaps in tsconfig.json or angular.json) might affect where this import statement imports from?
the problem in this case was that leaflet.markercluster ALSO needs #types installed. i had to issue a
npm install #types/leaflet #types/leaflet.markercluster
i don't know why the error had nothing to do with #types, but this is in keeping with the soul-crushing nature of Angular development.
In an Ember app, when using certain dependencies like moment installed via bower, we have to also import the same in the ember-cli-build.js file:
app.import('bower_components/moment/moment.js');
My question is why is that needed, since I would assume everything inside node_modules as well as bower_components should be available for use inside the app.
Also if that is not the case, how do we identify which dependencies would require such explicit import to be able to use them ?
You don't have to, actually.
There is a package now that lets you 'just import' things: https://github.com/ef4/ember-auto-import
Some reading on the topic of importing: https://discuss.emberjs.com/t/readers-questions-how-far-are-we-from-being-able-to-just-use-any-npm-package-via-the-import-statement/14462?u=nullvoxpopuli
In in-depth answer to your question and the reasons behind why things are the way they are is posted here:
https://discuss.emberjs.com/t/readers-questions-why-does-ember-use-broccoli-and-how-is-it-different-from-webpack-rollup-parcel/15384?u=nullvoxpopuli
(A bit too long for stack overflow, also on mobile, and I wouldn't want to lose all the links and references in a copy-paste)
Hope this helps
Edit:
To answer:
I just wanted to understand "in what cases" do we need to use the import statement in our ember-cli-build (meaning we do not do import for all the dependencies we have in our package/bower.json)...But only for specific ones...I wanted to know what is the criteria or use case for doing import.
Generally, for every package, hence the appeal of the auto-import and / or packagers (where webpack may be used instead of rollup in the future).
Though, it's common for ember-addons to define their own app.import so that you don't need to configure it, like any of these shims, specifically, here is how the c3 charting library is shimmed: https://github.com/mike-north/ember-c3-shim/blob/master/index.js#L7
Importing everything 'manually' like this is a bit of a nuisance, but it is, in part, due to the fact that js packages do not have a consistent distribution format. There is umd, amd, cjs, es6, etc.
with the rollup and broccoli combo, we need to manually specify which format a file is. There are some big advantages to the rollup + broccoli approach, which can be demonstrated here
and here
Sometimes, depending on the transform, you'll need a "vendor-shim".
These are handy when a module has decided it wants to be available on the window / global object instead of available as a module export.
Link: https://simplabs.com/blog/2017/02/13/npm-libs-in-ember-cli.html
(self represents window/global)
however, webpack has already done the work of figuring out how to detect what format a js file is in, and abstracts all of that away from you. webpack is what ember-auto-import uses, and is what allows you to simply
import { stuff} from 'package-name';. The downside to webpack is that you can't pipeline your transforms (which most people may not need, but it can be handy if you're doing Typescript -> Babel -> es5).
Actually: (almost) everything!
Ember does, by default, not add anything to your app except ember addons. There are however some addons that dynamically add stuff to your app like ember-browserify or ember-auto-import.
Also just because you do app.import this does not mean you can use the code with import ... from 'my-package'. The one thing app.import does is it adds the specified file to your vendor.js file. Noting else.
How you use this dependency depends completely on the provided JS file! Ember uses loader.js, an AMD module loader. So if the JS file you app.imported uses AMD (or UMD) this will work and you can import Foo from 'my-package'. (Because this is actually transpiled to AMD imports)
If the provided JS file provides a global you can just use the global.
However there is also the concept of vendor-shims.. Thats basically just a tiny AMD module you can write to export the global as AMD module.
However there are a lot of ember addons that add stuff to your app. For example things like ember-cli-moment-shim just exist to automagically add a dependency to your project. However how it's done completely depends on the addon.
So the rule is:
If its an ember addon read the addon docs but usually you shouldn't app.import
In every other case you manually need to use the library either by app.import or manual broccoli transforms.
The only exception is if you use an addon that tries to generically add dependencies to your project like ember-browserify or ember-auto-import
I am working on converting a large application from JavaScript (Backbone and Angular 1) to TypeScript. When we convert a file that is used by other files we understand that we have to update the import statements in those other JavaScript files so that it imports the new TypeScript file correctly. Our syntax update in fake-file.js is as follows.
Before:
import OurService from 'our.service';
After:
import { OurService } from 'our.service';
I understand that this is an easy change but TypeScript is new to many developers and there have been problems with people missing some of these import statements or forgetting to change them all together resulting in some issues during runtime. I have looked into compiler options but I do not see any that would fix this issue but I could be misinterpreting them.
Question: Is there a way to configure the compiler (or a Visual Studio Code plugin) to throw a warning or an error to prevent this from happening?
I assume that I understood your requirement and possibly you need to adapt a linting process and consequently I would suggest the following tools (which I also use in my project):
Airbnb Javascript style guide (your import statement concern-https://github.com/airbnb/javascript#modules). These are a well-defined set of standards defined for any JS application (including ES).
ESLint. You can run ESLint from the terminal and configure it for your project that highlights warning/errors in your code. If this looks complicated, you can generate the tslint document for your entire project in the website itself. Click on rules configuration and configure the ES rules for your project. There are some import related rules too.
PS: Feel free to add your comments.