crowmagnumb
crowmagnumb

Reputation: 7107

Linking Angular4 Module to Angular4 app: Unexpected value 'TestModule' imported by the module 'AppModule'. Please add a @NgModule annotation

See FURTHER UPDATE at bottom...

I have a very large app that I am trying to convert from Angular 1.6 to Angular 4.0. And it has become a herculean task and I am stuck at a particular point. We have a common set of utilities that we use between two different apps. In trying to get that to compile to a module that can be imported through "npm link" I get the following error upon importing into my app.

I am using webpack 3.

compiler.es5.js:1690 Uncaught Error: Unexpected value 'TestModule' imported by the module 'AppModule'. Please add a @NgModule annotation. at syntaxError (compiler.es5.js:1690) at compiler.es5.js:15382 at Array.forEach (<anonymous>) at CompileMetadataResolver.getNgModuleMetadata (compiler.es5.js:15365) at JitCompiler._loadModules (compiler.es5.js:26795) at JitCompiler._compileModuleAndComponents (compiler.es5.js:26768) at JitCompiler.compileModuleAsync (compiler.es5.js:26697) at PlatformRef_._bootstrapModuleWithZone (core.es5.js:4536) at PlatformRef_.bootstrapModule (core.es5.js:4522) at Object.<anonymous> (main.ts:26)

I have created a test case that minimally recreates my issue. It looks a little more complex because I left my webpack.config and package.json files roughly the same as my real life cases but the code is reduced to a minimum.

You can see the code https://github.com/crowmagnumb/ng-migration-test and the steps to recreate the issue exactly.

My main.ts file in my app looks like this...

import './polyfills';

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {UpgradeModule} from '@angular/upgrade/static';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

import {TestModule} from 'ng-module-test';

@NgModule({
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        UpgradeModule,
        TestModule
    ]
})
export class AppModule {
    // Override Angular bootstrap so it doesn't do anything
    ngDoBootstrap() {
    }
}

// Bootstrap using the UpgradeModule
platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef => {
    console.log("Bootstrapping in Hybrid mode with Angular & AngularJS");
    const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
    upgrade.bootstrap(document.body, ['au.website']);
});

... and that of my module looks like ...

import {NgModule} from '@angular/core';

import {Utils} from './utils/module';

@NgModule({
    imports: [
        Utils
    ]
})
export class TestModule {};

Thanks for your help.

UPDATE

I changed my import of TestModule to be a relative path instead of loading through the linked npm module like so ...

// import {TestModule} from 'ng-module-test';
import {TestModule} from '../../ng-module-test/src/main';

... and I get the same result. So it doesn't appear to have anything to do with npm link but rather the specific separation of code that entails. Can anyone shed light on that?

I've checked that change into the above repo.

FURTHER UPDATE

Now I totally don't understand this. If I change the import to read ...

import {TestModule} from './ng-module-test/main';

... and I make a directory called ng-module-test inside of ng-app-test/src and I copy the contents of the ng-module-test/src folder into that then it works as @Hosar kindly discovered in the comments. BUT if I make a symlink using ln -s ../../ng-module-test/src ng-module-test to the code so that effectively from the standpoint of the underlying OS the exact same thing, IT DOESN"T WORK.

Upvotes: 0

Views: 1237

Answers (2)

crowmagnumb
crowmagnumb

Reputation: 7107

I found a solution to the symlink direction. Just need to set symlinks to false (counter-intuitively) in the resolve section of webpack.config.js. For example ...

resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    symlinks: false
},

But this still does not allow me to use npm link as that still fails. But this is fine for now to get me going forward. In some ways it's better since now the watch system will pick up changes in my typescript files automatically. But it doesn't provide for much flexibility should I decide I do want/need to publish this library. But if I figure that out later it would be just a global search and replace to fix it.

So, in the end, it looks like I will add a symlink in my apps' src dir pointing to the src directory of my library. And then I will import stuff from that library through ...

import {Blah} from './ng-module-test/main';

... or (e.g) ...

import {Blah} from '../../ng-module-test/main';

... depending on how deep into the src dir the file I'm importing into is. I'm going to look into the modules property of webpack config to see if I can just address it as ...

import {Blah} from 'ng-module-test/main';

... everywhere. Or better yet even drop the main. I'll give that solution if I manage it.

UPDATE

Better solution. Thanks to this link I was able to realize a better solution that doesn't involve symlinks (unless you want to use one instead of putting relative path to the library module I suppose).

If I add the following to my tsconfig.json file:

"paths": {
    "NgModuleTest/*": ["../ng-module-test/src/*"]
}

... and then change resolve in my webpack.config.js to ...

const TsConfigPathsPlugin = require('awesome-typescript-loader').TsConfigPathsPlugin;

resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    // symlinks: false,
    modules: [
        `${__dirname}/node_modules`
    ],
    plugins: [
        new TsConfigPathsPlugin({
            tsconfig: './tsconfig.json',
            compiler: 'typescript'
        })
    ]
}

... I can then reference imports to my external library via ...

import {Blah} from 'NgModuleTest/main';

... no matter where I am in the src tree. Plus I can add as many of these paths as I want so that I can reference internal modules in the same easy way. Nice. I did not test to see if paths in tsconfig.json accepts a symlink or not, as I said, and what that means for the symlink property of resolve. But I'm fine with using the tsconfig.json to point to code outside of that directory as I am doing above.

Oh, by the way, I moved tsconfig.json out of src into the root to be alongside webpack.config.js because I had some path issues that seemed like it would be solved with them in the same location. And it did. I think this might be necessary so that the plugin has the same relative path as the tsconfig.json.

Updated repo above with this solution.

Upvotes: 1

Simon Briggs
Simon Briggs

Reputation: 1269

You need to make all the @angular packages a peerDependency and devDependency instead of a dependency in ng-module-test/package.json

Then add the following to ng-app-test/src/tsconfig.json:

{
  "compilerOptions": {
    // ...
    // Note: these paths are relative to `baseUrl` path.
    "paths": {
      "@angular/*": [
        "../node_modules/@angular/*"
      ]
    }
  }
}

See full explanation: https://github.com/angular/angular-cli/wiki/stories-linked-library

Upvotes: 1

Related Questions