Migrating from aurelia-bundler + jspm to dumber.js (Aurelia 1)

Greetings.
I am currently trying to migrate our Aurelia 1 project from the very old aurelia-bundler (not the cli-bundler but the one used by old skeletons) + jspm to dumber.js, since it apparently supports Aurelia 1 with aurelia-deps-finder. We use TypeScript with tslib.

I have managed to at the very least build something, but there seem to be several problems with dependency injection which prevent the project from even starting:

  • “Uncaught (in promise) TypeError: Class extends value undefined is not a constructor or null”
    This is called by __extends from tslib, the constructor function of the superclass is undefined at runtime.
  • aurelia.use.standardConfiguration().globalResources(...) in main.configure breaks if some of these global resources need dependency injection (possibly only if said classes are not global, I have not checked):
@autoinject
export class SomeValueConverter { // this is in globalResources()
constructor(private service: SomeService) {} // the service is not in globalResources
}

This is the error heading:

Uncaught (in promise) Error: Error invoking LocalizeFloatValueConverter. Check the inner error for details.
Message: key/value cannot be null or undefined. Are you trying to inject/register something that doesn't exist with DI?
  • If for some reason the above errors do not happen (I think some bundles are loaded/executed in a non-deterministic order), the views cannot be loaded because they are not included in the bundles. This is definitely due to our unusual project structure: we do NOT and cannot use plain .html views because they need to be dynamically generated in the backend via Razor (ASP.NET Core). Is there a way to instruct the bundler (i assume via aurelia-deps-finder) to not try to include and load these from the bundles? (There might be more, but as I said I cannot reach this point reliably due to the previous errors.)

Please note that none of these problems occur with the legacy builder, which however we would like to migrate away from because of its age and some limitations (e.g. lack of ES6 syntax support).

Regards.

1 Like

Sorry for late reply. I will look into it today.

Meanwhile, if you can share a minimal repo to reproduce the error, that would be helper for the investigation.

1 Like

I am not sure I can provide a minimal reproduction since our system is quite complex and stripping out the unneeded parts is not easy.

I can however provide some pointers:


Problem 2 seems to be solved by explicitly registering the required component, though I do not know why this was not needed before

aurelia.use
        .standardConfiguration()
        .singleton(ApplicationState, ApplicationState)
        .globalResources([
            "helpers/valueConverters"
        ])

I do not know why this was not needed before. For reference here is the ApplicationState initialization (which I have not changed).

@autoinject
export class ApplicationState {
    constructor(
        private http: HttpClient,
        private loader: Loader,
        private router: Router
    ) {
        // do something
    }
}

Maybe some of these modules have some special requirements or need to be initialized / registered in a special way?


Problem 3 is definitely due to our unorthodox view location mode. I have to look into how to configure it. As of now I have noticed the old version used SystemJS and full URLs to identify modules while the new version only uses a local module path.


Problem 1 does not seem to be present after solving problem 2 and putting tslib in the prepend section.

More on problem 3.

This is the relevant part of aurelia, which loads the module:

  DefaultLoader.prototype._import = function (moduleId) {
    return new Promise(function (resolve, reject) {
      _aureliaPal.PLATFORM.global.require([moduleId], resolve, reject);
    });
  };

I assume require is dumber’s module loader.
The viewmodel is resolved correctly, but the view is not.

The main problem is that in our case the viewmodel and view have different paths:

  • viewmodel (ts): ~/dist/[moduleId]
  • view (cshtml): ~/templates/[moduleId]

Note that our views are NOT bundled; they are generated via Razor (ASP .NET Core) on the fly.

This is not a problem when using the legacy aurelia-bundler, which uses SystemJS and uses a full URL as moduleId: in this case it is possibile to override ViewLocator.prototype.convertOriginToViewUrl with our convention.

In short, is it possible to instruct the loader to look for viewmodels and views in different places?

Another update: my current plan is trying to adapt DefaultLoader and aurelia-bootstrapper to our needs; however due to other priorities this migration will have to wait.

Thank you anyway for your attention.

I found this is a bug in dumber-module-loader, it behaves different than requirejs when loading an absolute module id “./foo” (requirejs treats requirejs("./foo") as if it’s requirejs("foo")).

That causes the edge case when using globalResources() in src/main.

.globalResources(['helper/valueConverters']); // this works
.globalResources(['./helper/valueConverters']); // this does not

In comparison, globalResources() call in plugin index file works fine, because it’s loaded by requirejs as a relative module (resource ./foo/bar related to plugin-name is resolved to module plugin-name/foo/bar). But the usage in src/main.ts is trying to load it as an absolute module.

I have now fixed the dumber-module-loader bug. You can do a fresh npm install (remove node_modules and package-lock.json). Those DI and view things should work.

Let us see if there is other issue surfaced up.

2 Likes

As for the remote view template loading, if all your html templates are in folder templates/,
You can force the paths mapping in dumber’s option for any module starting with “templates/…”.

paths: {
  'templates': '/templates'
}

This mapping to absolute path will force it to ignore baseUrl setting (/dist by default).

2 Likes

The path option does not seem to work, though our templates do NOT actually start with templates; I might have described our use case incorrectly.

As an example, one of my modules is found at widgets/AutoComplete and called such.
What the application currently does, with the legacy aurelia-bundler and a specific configuration in ViewLocator (as described in the documentation), is:

  • load the viewmodel from /dist/widgets/AutoComplete.js (actual file or mapped to a bundle)
  • load the view from /templates/widgets/AutoComplete (NOT a file in the file system, NOT static but generated by Razor at runtime)

Again, the /templates is not part of the module name but it is added by the ViewLocator (I guess I should have posted this snippet sooner, I think it was adapted from the old skeleton):

ViewLocator.prototype.convertOriginToViewUrl = (origin: Origin) => {
    const moduleId = origin.moduleId;
    return ((moduleId.endsWith(".js") || moduleId.endsWith(".ts"))
        ? moduleId.substring(0, moduleId.length - 3)
        : moduleId).replace("dist/", "templates/");
}

From my understanding, Aurelia’s (generic) module loading process should be something like this:

  1. a module gets required by a dependent
  2. the JS module (standalone .js file or bundled) is found and loaded
  3. the corresponding view gets loaded

What I want to do is intervening between steps 2 and 3 to tell the loader something like:

  1. When you are required to load module widgets/AutoComplete:
  2. load its viewmodel from the URL /dist/widgets/AutoComplete.js as usual BUT
  3. INSTEAD of loading the view from the URL you are programmed to know - that is, /dist/widgets/AutoComplete.html - load it from the URL /templates/widgets/AutoComplete with no file extension.

If this was already obvious I apologize.

So far I have tried with custom forks of aurelia-default-loader and aurelia-bootstrapper, but to no avail. I have also tried following the code in dumber-module-loader, but it is both quite complex and difficult to debug since it gets embedded in an already minified state.

1 Like

Sorry for late reply.
I have not used ViewLocator. I will do some testing and get back to you. It’s probably because I never used this feature, so I missed something in dumber’s Aurelia support.

2 Likes

No problem.
For a more specific reference (and kind of a proof that something like our method is somewhat officially endorsed), this is our approach (first section).

2 Likes

From the original ViewLocator, I think you removed return id + '.html';, why?

1 Like

Here is a demo for using ViewLocator with dumber bundler with the help of “paths” options.

3 Likes

I cannot speak for the original developer of this part, but I am pretty sure this is because our views are dynamically generated from an ASP .NET Core controller and therefore have no natural extension.

However, by configuring our controller to accept and strip away the .html extension the locator works correctly. I guess this is more of a general convention problem of what to do with extensionless paths.

Some early inputs and notes for now, which I will have to investigate and try more thoroughly:

  • the globalResources problem I noted earlier seems to still be present with dumber-module-loader@1.2.1 (I checked the version in its package.json), implicitly required by gulp-dumber.
  • there seem to be some problems with identifying some resources required in the HTML with <require>; they probably do not get picked up because the bundler is not instructed to look in our .cshtml files. I guess moving these requirements in the .ts files should solve the issue.
  • something that does not seem to work at all is dynamic module loading with the Loader class exported by aurelia-framework (which should be DefaultLoader): when using dumber, is this class used at all? I guess these should be replaced with references to dumber’s global requirejs().

As always thank you very much for your attention.

1 Like

If you can share a minimum working repo that reassemble your production setup in CLI Bundler, I can check why it didn’t work on dumber.
For those dynamic generated views, you can use the nodejs dev-server to mock up I guess.

In gulpfile, you might need to add *.cshtml to the list of sources.
Dumber does not touch any Aurelia internally, the dumber-module-loader is an AMD loader which Aurelia supports through aurelia-loader-default package.

I had some time today to look into this more carefully and I wanted to share some notes with those interested:

  • I fully deleted node_modules, ran the clear-cache job and reinstalled the packages; now the globalResources problem does not occur anymore.
  • The resources in .cshtml files were CSS files from external modules; simply replacing those requires with imports in the corresponding .ts viewmodels works.
  • The loader works, the problem is with some dynamic runtime dependencies (specifically, locales for Moment.js)
  • I have noticed some errors with circular dependencies which caused no problems with jspm / aurelia-bundler. I have solved these by simply finding and removing the dependencies. I have found this tool for detecting these cases; I guess those interested could include it or a similar tool as a build step.
  • tslib does not seem to work when imported, but it works when included in the prepend list.

As for the aforementioned dynamic runtime dependencies, I have looked into the documentation and found the deps option which seems what I need, but I cannot manage to find a way to include specific files.

For reference:

  • official Moment guide for including locales
  • single locales are found in the node_modules/moment/locale/[locale_code].js path
  • my current implementation with jspm loads these at runtime via defaultLoader.loadModule("moment/locale/[locale_code]") with no issues

How do I include these files in the deps configuration (assuming it is the right place), so that they are available via loadModule("moment/locale/[locale_code]")?

I have tried some combinations with no success, but I figured asking here could help someone with a similar problem.

For future reference, I plan to switch to Day.js once some dependency issues are solved; its locales should work the same as Moment’s.

As a follow-up for the above, for now I have decided to just manually import the few locales we need, in which case loadModule works correctly. I guess there is not much difference between this and meddling with deps/append/prepend.

Those deps/append/prepend are for legacy or weird packages that doesn’t follow standard, you don’t need to use them.

Status update.
I have upgraded to the latest dumber version: the major issues I had seem solved, and I finally managed to produce a working configuration.
This issue can be considered solved. Thanks you for your work and attention.

A relevant note is that my problems with tslib and prepend were due to the fact I used noEmitHelpers; switching it to importHelpers solved my compatibility issue (notably with some d3 packages).

3 Likes

Some more notes I noticed which might help those in need:

  • in tsconfig.json I vaguely recall esModuleInterop and simplified imports being necessary for some packages (e.g. moment), but I am not 100% sure
  • references to components in html <require from"..."></require> are case-sensitive even under Windows. This was either not the case with jspm or hidden by the fact we always deployed the entire jspm_packages folder just to be sure
  • modules included via Loader.loadModule (default Aurelia module loader) are NOT automatically traced and have to be included in the deps array
2 Likes