How to use ng-annotate with hybrid app based on angular-cli - angularjs

I'm working on an Angular.js project written with TypeScript. We're trying to evaluate whether to upgrade to Angular 8 and we're stuck with how to use ng-annotate with angular-cli's webpack configuration.
I believe that this can be achieved either by using the #angular-builders/custom-webpack tool or by using ngx-build-plus tool but I had not succeeded with neither of them.
My current attempt includes a partial webpack file for ngx-build-plus with the following configuration :
module: {
rules: [
{
test: /\.ts$/,
loaders: ['ng-annotate-loader?ngAnnotate=ng-annotate-patched'],
},
{
test: /\.tpl\.html$/,
loader: 'ng-cache-loader?-url&module=templates&prefix=src:./**/'
}
]
},
};
Having this, when I run ng serve --extra-webpack-config webpack.partial.js -o I get the following error : NonErrorEmittedError: (Emitted value instead of an instance of Error) error: couldn't process source due to parse error,Unexpected token
The token to which it refers, is simply the type declaration for a method parameter. So I'm guessing that there is some conflict with the loader that angular-cli already uses for TypeScript files but I don't know how to resolve this.
Is there any input on how to solve this either using one of the two tools or something else?

So, the way to do this is by using webpack-merge and custom-webpack.
This is the configuration to run ng-annotate with Typescript files :
module.exports = (config, options) => {
const customConfig = {
module: {
rules: [
{
test: /\.ts$/,
loaders: ['ng-annotate-loader?ngAnnotate=ng-annotate-patched'],
},
{
test: /\.tpl\.html$/,
loader: 'ng-cache-loader?-url&module=templates&prefix=src:./**/'
}
]
}
};
return merge.strategy({
'module.rules': 'prepend'
})(config, customConfig)
};
The key part is the merge.strategy call which will make sure that the loaders of the custom configuration will be prepended to the ones that angular-cli already sets up.

After several hours of looking at alternatives, the solution that worked the best for me was using babel-plugin-angularjs-annotate by:
Creating a new project
Installing babel and babel-plugin-angularjs-annotate
Executing babel, which takes the files from src, adds the annotations, and puts the result in the folder output.
For others needing the complete solution, this is what I executed:
mkdir babel-project && cd babel-project
npm init -y
npm install -D babel-cli babel-plugin-angularjs-annotate
Create .babelrc and add this:
{
"presets": [],
"plugins": [ "angularjs-annotate" ]
}
Finally execute babel:
npx babel src -d build --extensions .ts
This takes files like:
import * as _ from "underscore";
import "#app2/hello/chao"
const greeting = () => "Hello World"
angular.module("MyMod")
.controller("MyCtrl", ($scope, HelloClient) => {
HelloClient.add($scope, greeting);
});
And turns them into:
import * as _ from "underscore";
import "#app2/hello/chao";
const greeting = () => "Hello World";
angular.module("MyMod").controller("MyCtrl", ["$scope", "HelloClient", ($scope, HelloClient) => {
HelloClient.add($scope, greeting);
}]);
Update
Using babel-plugin-angularjs-annotate messed a lot with my formatting, so I ended up copying&pasting just the relevant parts of the Babel output, which took some hours.

Related

How can I fix the "Failed to parse source map from ..." errors in my ReactJS/TS project

I have a ReactJS project where I wanted to use a Barcode-Scanner npm module with the name html5-qrcode, but I always get this error:
Failed to parse source map from 'C:\...\node_modules\html5-qrcode\third_party\index.js.map' file: Error: ENOENT: no such file or directory, open 'C:\...\node_modules\html5-qrcode\third_party\index.js.map'
And then there are errors like: (seperated for readability)
WARNING in ./node_modules/html5-qrcode/esm/camera/core-impl.js Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from 'C:\...\node_modules\src\camera\core-impl.ts' file: Error: ENOENT: no such file or directory, open 'C:\...\node_modules\src\camera\core-impl.ts'
I thought it might be an TS error, because every file of the second error part has a .ts ending.
So I made a new ReactTS project with all components and co in it, but I still get the same error.
I thought it might be an TS error, because every file of the second error part has a .ts ending. So I made a new ReactTS project with all components and co in it, but I still get the same error.
It seems like the npm package has issues with its source maps and webpacks's source-map-loader module is not able to process them. This doesn't really affect the application itself but having all those warnings is annoying.
I came across two solutions: either force the source-map-loader to skip the culprit package or ignore source-map warnings all together.
To achieve either solution, you'll need to be able to override the webpack.config.js. How to override it really depends on the framework you use to run your React apps (I have mine setup using NX)
Solution 1: Ignore source-mapping warnings (Easiest)
Add ignoreWarnings: [/Failed to parse source map/] to your webpack configuration.
E.g.
const { merge } = require('webpack-merge');
module.exports = (config) => {
return merge(config, {
ignoreWarnings: [/Failed to parse source map/]
});
};
Your webpack.config.js will look a lot different than this.
The idea is to add (or override) ignoreWarnings with the pattern of the message it should ignore.
Solution 2: Skip source-map-loading for culprit package (Cleanest?)
const { merge } = require('webpack-merge');
module.exports = (config, context) => {
return merge(config, {
module: {
rules: [
{
enforce: 'pre',
test: /\.js$/,
use: [
{
loader: 'source-map-loader',
options: {
filterSourceMappingUrl: (url, resourcePath) => {
// #zxing has issues with its source maps
if (/#zxing/i.test(resourcePath)) {
return false;
}
return true;
}
}
}
]
}
]
}
});
};
The idea here is to override the source-map-loader rules and skip its execution if the current resource matches the regex. In my case, I want to skip any resource that contains #zxing.
I tried using hte exclude option but I had no luck with that and opted to use the filterSourceMappingUrl instead. Maybe it works for you though. Remember, the pathing has to be absolute so you might need to adapt the excluded pathings.
More details here
const { merge } = require('webpack-merge');
module.exports = (config, context) => {
return merge(config, {
module: {
rules: [
{
enforce: 'pre',
test: /\.js$/,
use: ['source-map-loader'],
exclude: ['/node_modules/#zxing']
}
]
}
});
};
Hope this helps.
Cheers.
add react-app-rewired to your project:
yarn add --dev react-app-rewired
or
npm install react-app-rewired --save-dev
modify your package.json with those lines:
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
create a file called config-overrides.js in project root (same folder as package.json) with this content:
module.exports = function override(config) {
return {
...config,
ignoreWarnings: [
{
module: /node_modules\/stylis-plugin-rtl/,
},
],
}
}
change "stylis-plugin-rtl" in config-overrides.js above to whichever name of library that has an invalid build and throwing errors.
source maps errors should no longer appear.

React with TypeScript using tsyringe for dependency injection

I am currently having trouble with my React TypeScript project.
I created my project with npx create-react-app my-app --template typescript.
I recently added tsyringe for dependency injection and was trying to implement it for an apiService. After following the readme(https://github.com/microsoft/tsyringe#injecting-primitive-values-named-injection) for adding primitive values I have hit a block. I already add experimentalDecorators and emitDecoratorMetadata to my tsconfig.json file with no success.
The error actual error I am encountering is:
./src/ts/utils/NetworkService.ts 9:14
Module parse failed: Unexpected character '#' (9:14)
File was processed with these loaders:
* ./node_modules/#pmmmwh/react-refresh-webpack-plugin/loader/index.js
* ./node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
|
| let NetworkService = (_dec = singleton(), _dec(_class = (_temp = class NetworkService {
> constructor(#inject('SpecialString')
| value) {
| this.str = void 0;
I am fairly sure this problem is caused by Babel, however I created this with npm create react-app --template typescript and do not seem to have access to the Babel configuration.
NetworkService.ts
#singleton()
export default class NetworkService
{
private str: string;
constructor(#inject('SpecialString') value: string) {
this.str = value;
}
}
Invocation method
bob()
{
const inst = container.resolve(NetworkService);
}
Registering Class in index.ts
container.register('SpecialString', {useValue: 'https://myme.test'});
#registry([
{ token: NetworkService, useClass: NetworkService },
])
class RegisterService{}
React-Scripts manages many of the configs related to the project. For many cases, this is fine and actually a nice feature. However, because React-Scripts uses Babel for it's development environment and does not expose the config.
You have to run npm run eject to expose the configurations.
Please note, this is a one-way operation and can not be undone.
Personally, I prefer more control with my configuration.
After this you can edit the webpack.config.js in the newly created config folder.
Find the section related to the babel-loader in the dev-environment and add 'babel-plugin-transform-typescript-metadata' to the plugins array.
Expanding on Jordan Schnur's reply, here are some more pitfalls I encountered when adding TSyringe to my CRA app:
Use import type with #inject
If you get this error "TS1272: A type referenced in a decorated signature must be imported with 'import type' or a namespace import when 'isolatedModules' and 'emitDecoratorMetadata' are enabled." replace import with import type for the offending imports. You will encounter this when working with #inject
E.g. replace import { IConfig } from "iconfig" with import type { IConfig } from "iconfig"
Fixing Jest
Your Jest tests will also break with TSyringe, especially when using #inject. I got the error "Jest encountered an unexpected token" with details constructor(#((0, _tsyringe.inject)("")) ("#" marked as the offending token). I took the following steps to fix that in CRA:
Add the line import "reflect-metadata"; to the top of the file src/setupTests.ts
In config/jest/babelTransform.js replace line 18 and following:
From
module.exports = babelJest.createTransformer({
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
babelrc: false,
configFile: false,
});
to:
module.exports = babelJest.createTransformer({
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
plugins: [
require.resolve('babel-plugin-transform-typescript-metadata')
],
babelrc: false,
configFile: false,
});
Instead of eject, you may use a lib that "overrides" some of your params.
I used craco : https://www.npmjs.com/package/#craco/craco
I've created an simpler DI library that doesn't need decorators or polyfill. Works with CRA like a charm and has cool React bindings
iti
import { useContainer } from "./_containers/main-app"
function Profile() {
const [auth, authErr] = useContainer().auth
if (authErr) return <div>failed to load</div>
if (!auth) return <div>loading...</div>
return <div>hello {auth.profile.name}!</div>
}

TypeScript constructor parameter with modifiers doesn't emit correctly with ts-loader

I was starting to work with a web application built with TypeScript/React/Storybook. I noticed when I write a class:
class MyClass {
constructor(public a) { }
}
console.log(MyClass);
and run the unit tests with jest, or using tsc to emit code, it generates the expected JS code like:
function MyClass(a) {
this.a = a;
}
But when I run it in a Storybook app with development server, it prints out the JS code like:
function MyClass(a) {
_classCallCheck(this, MyClass);
}
I'm thinking if it's related to the TypeScript compiler version picked by ts-loader, but cannot figure it out. I checked the TypeScript version installed to my node_modules folder, and it's v4.1.2, which looks nothing wrong with me.
Also I mentioned Playbook here (and left "playbook" in the tags) only because my app is with it. It might not be directly related to this issue.
Seems like an issue with recent versions #babel/preset-typescript if you are using it. You can change your babel configuration to use "#babel/plugin-transform-typescript" plugin instead of #babel/preset-typescript preset.
More info: https://www.gitmemory.com/issue/babel/babel/8752/486541662
If generated by storybook, your babel config probably looks like:
module.exports = {
presets: [
["#babel/preset-env", { targets: { node: "current" } }],
"#babel/preset-typescript",
],
};
Then change it to:
module.exports = {
presets: [["#babel/preset-env", { targets: { node: "current" } }]],
plugins: ["#babel/plugin-transform-typescript"],
};

react lazy load external amd modules

My react app has external resources outside src/ so i have ejected react-scripts and disabled ModuleScopePlugin.
Referenced the external library in resolve.alias and used across the application.
resolve.alias: {
'genlib': path.resolve(fs.realpathSync(process.cwd()), 'lib/genlib/js/src'),
'config': path.resolve(fs.realpathSync(process.cwd()), 'config/dev'),
'messages': path.resolve(fs.realpathSync(process.cwd()), 'config/messages')
}
genlib is the external library im trying to reference.
The external library is AMD using requirejs.
One of the file in the library lazy loads a class using require.
define('class1', ['require', ...], function(require, ...) {
//
require([variable], function()...)
});
The above require is throwing Cannot find module 'xxx' at runtime from webpackEmptyContext.
When require from above code is consoled then below is logged instead of require function. Confused why webpackEmptyContext is consoled out instead of webpackContext function
ƒ webpackEmptyContext(req) {
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
I have not changed any of the webpack.config.js except adding alias and disabling ModuleScopePlugin.
What else needs to be added or changed in config to lazy load amd modules.
webpack v4.19.1
react-dev-utils v7.0.1
I have solved by using ContextReplacementPlugin.
Added below code to webpack config plugins.
new webpack.ContextReplacementPlugin(/genlib[\\/]services/, /.*$/),
Now a map is created with all the files in the services directory and webpackContext loads the files when required.
You will see babel-loader in return object of webpack.config.js file. module -> rules array First code is to run the linter
{
test: /\.(js|mjs|jsx)$/,
enforce: 'pre',
use: [
{
options: {
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
},
loader: require.resolve('eslint-loader'),
},
],
include: [
paths.appSrc,
'paht/to/external-library/using/requirejs' <---- Add your external file path for loader to parse the AMD files
],
}
Similarly include file path to test entry of JS files
test: /\.(js|mjs|jsx|ts|tsx)$/,
Can you try this and check?

How to use jest with webpack?

I use webpack to develop a React component. Here is a simple version of it:
'use strict';
require('./MyComponent.less');
var React = require('react');
var MyComponent = React.createClass({
render() {
return (
<div className="my-component">
Hello World
</div>
);
}
});
module.exports = MyComponent;
Now, I would like to test this component using jest. Here is the relevant bit from my package.json:
"scripts": {
"test": "jest"
},
"jest": {
"rootDir": ".",
"testDirectoryName": "tests",
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
"unmockedModulePathPatterns": [
"react"
]
}
When running npm test, I get the following error:
SyntaxError: /Users/mishamoroshko/react-component/src/tests/MyComponent.js: /Users/mishamoroshko/react-component/src/MyComponent.js: /Users/mishamoroshko/react-component/src/MyComponent.less: Unexpected token ILLEGAL
Looks like webpack needs to process require('./MyComponent.less') before jest can run the test.
I wonder if I need to use something like jest-webpack. If yes, is there a way to specify multiple scriptPreprocessors? (note that I already use babel-jest)
The cleanest solution I found for ignoring a required module is to use the moduleNameMapper config (works on the latest version 0.9.2)
The documentation is hard to follow. I hope the following will help.
Add moduleNameMapper key to your packages.json config. The key for an item should be a regex of the required string. Example with '.less' files:
"moduleNameMapper": { "^.*[.](less|LESS)$": "EmptyModule" },
Add a EmptyModule.js to your root folder:
/**
* #providesModule EmptyModule
*/
module.exports = '';
The comment is important since the moduleNameMapper use EmptyModule as alias to this module (read more about providesModule).
Now each require reference that matches the regex will be replaced with an empty string.
If you use the moduleFileExtensions configuration with a 'js' file, then make sure you also add the EmptyModule to your 'unmockedModulePathPatterns'.
Here is the jest configuration I ended up with:
"jest": {
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
"moduleFileExtensions": ["js", "json","jsx" ],
"moduleNameMapper": {
"^.*[.](jpg|JPG|gif|GIF|png|PNG|less|LESS|css|CSS)$": "EmptyModule"
},
"preprocessorIgnorePatterns": [ "/node_modules/" ],
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react",
"<rootDir>/node_modules/react-dom",
"<rootDir>/node_modules/react-addons-test-utils",
"<rootDir>/EmptyModule.js"
]
}
I ended up with the following hack:
// package.json
"jest": {
"scriptPreprocessor": "<rootDir>/jest-script-preprocessor",
...
}
// jest-script-preprocessor.js
var babelJest = require("babel-jest");
module.exports = {
process: function(src, filename) {
return babelJest.process(src, filename)
.replace(/^require.*\.less.*;$/gm, '');
}
};
But, I'm still wondering what is the right solution to this problem.
I just found that it's even simpler with Jest's moduleNameMapper configuration.
// package.json
"jest": {
"moduleNameMapper": {
"^.+\\.scss$": "<rootDir>/scripts/mocks/style-mock.js"
}
}
// style-mock.js
module.exports = {};
More detail at Jest's tutorial page.
I recently released Jestpack which might help. It first builds your test files with Webpack so any custom module resolution/loaders/plugins etc. just work and you end up with JavaScript. It then provides a custom module loader for Jest which understands the Webpack module runtime.
From Jest docs:
// in terminal, add new dependency: identity-obj-proxy
npm install --save-dev identity-obj-proxy
// package.json (for CSS Modules)
{
"jest": {
"moduleNameMapper": {
"\\.(css|less)$": "identity-obj-proxy"
}
}
}
The snippet above will route all .less files to the new dependency identity-obj-proxy, which will return a string with the classname when invoked, e.g. 'styleName' for styles.styleName.
I think a less hacky solution would be to wrap your preprocessor in a conditional on the filename matching a javascript file:
if (filename.match(/\.jsx?$/)) {
return babelJest.process(src, filename);
} else {
return '';
}
This works even if you don't explicitly set the extension in the require line and doesn't require a regex substitution on the source.
I have experienced similar issue with such pattern
import React, { PropTypes, Component } from 'react';
import styles from './ContactPage.css';
import withStyles from '../../decorators/withStyles';
#withStyles(styles)
class ContactPage extends Component {
see example at https://github.com/kriasoft/react-starter-kit/blob/9204f2661ebee15dcb0b2feed4ae1d2137a8d213/src/components/ContactPage/ContactPage.js#L4-L7
For running Jest I has 2 problems:
import of .css
applying decorator #withStyles (TypeError: <...> (0 , _appDecoratorsWithStyles2.default)(...) is not a function)
First one was solved by mocking .css itself in script preprocessor.
Second one was solved by excluding decorators from automocking using unmockedModulePathPatterns
module.exports = {
process: function (src, filename) {
...
if (filename.match(/\.css$/)) src = '';
...
babel.transform(src, ...
}
}
example based on https://github.com/babel/babel-jest/blob/77a24a71ae2291af64f51a237b2a9146fa38b136/index.js
Note also: when you working with jest preprocessor you should clean cache:
$ rm node_modules/jest-cli/.haste_cache -r
Taking inspiration from Misha's response, I created an NPM package that solves this problem while also handling a few more scenarios I came across:
webpack-babel-jest
Hopefully this can save the next person a few hours.
If you're using babel, you can strip unwanted imports during the babel transform using something like https://github.com/Shyp/babel-plugin-import-noop and configuring your .babelrc test env to use the plugin, like so:
{
"env": {
"development": {
...
},
"test": {
"presets": [ ... ],
"plugins": [
["import-noop", {
"extensions": ["scss", "css"]
}]
]
}
}
}
We had a similar problem with CSS files. As you mentioned before jest-webpack solves this problem fine. You won't have to mock or use any module mappers either. For us we replaced our npm test command from jest to jest-webpack and it just worked.
Webpack is a great tool, but I don't need to test it's behavior with my Jest unit tests, and adding a webpack build prior to running unit tests is only going to slow down the process. The text-book answer is to mock non-code dependencies using the "moduleNameMapper" option
https://facebook.github.io/jest/docs/webpack.html#handling-static-assets

Resources