Lernajs/yarn workspaces with an aurelia application

Here is what I came up with last week when I did some playing around with v1 loading custom elements from other modules in a yarn workspace.

It sounds like you’ve already worked out that the aurelia webpack plugin cannot load your components via conventions when they are outside the app root. It took me far too long to grasp this :man_facepalming:.

In my example app, the page component is in @timfish/module and it loads the html and scss like this:

import { inlineView, customElement } from 'aurelia-framework';
import html from './page.html';
import './page.scss';

@customElement('page')
@inlineView(html)
export class Page {
  public something: string = 'Loaded from Module!';
}

The build for the @timfish/module simply builds the TypeScript and copies the html/scss files to the output directory too. With the right declarations, TypeScript doesn’t complain about the html and scss imports :+1:.

When you want to use the page component in the app, <require from="..."> in html does not work. You have to either load it via globalResources or load it manually using the viewResources decorator:

import { viewResources } from 'aurelia-framework';
import { Page } from '@timfish/module';

@viewResources(Page)
export class App {
}

With a few keys bit in the webpack config, it’ll resolve your modules symlinked in the root node_modules and finds the html and scss where they are expected too:

    resolve: {
      extensions: ['.ts', '.js'],
      modules: ['src', 'node_modules', '../../node_modules'].map((p) =>
        resolve(__dirname, '..', p)
      ),
      symlinks: false,
    },
2 Likes

So do you not need to do aliases to resolve modules?

1 Like

No aliases required. The module is compiled to plain old JavaScript with the HTML and SCSS files copied alongside and Webpack resolves it like any other dependency because we tell it about also resolving from the root ../../node_modules.

2 Likes

I have to spend some time trying to figure out my setup vs yours. I made some of the changes, I get a working webpack build (It builds), but it’s not building any of my stuff…

1 Like

I managed to get it to load though I’m back to the point I was stuck at. I do like your cleaner approach though…

Would you be able to do me a favor. Can you install these 2 packages, I don’t think you need to do anything as it is the plugin trying to load the default css that is failing.
aurelia-syncfusion-ej2-bridge
@syncfusion/ej2

The bridge plugin can be configured as follows

 aurelia.use.plugin(PLATFORM.moduleName('aurelia-syncfusion-ej2-bridge'), (config: ConfigBuilder) => {
        config.useDefaults();
    });

I have not moved the bridge source to github yet, but my configBuilder class looks like this.

import { PLATFORM } from "aurelia-pal";

export class ConfigBuilder {
  public resources: any[] = [];
  public useGlobalResources: boolean = true;

  /**
  * Globally register all EJ wrappers including templating support
  */
  useAll(): ConfigBuilder {
    this
      .ej2Accordion()
      .ej2AutoComplete()
      .ej2Button()
      .ej2Checkbox()
      .ej2CheckboxList()
      .ej2ComboBox()
      .ej2DatePicker()
      .ej2DateRangePicker()
      .ej2DateTimePicker()
      .ej2DashboardLayout()
      .ej2DropDownList()
      .ej2Grid()
      .ej2ListView()
      .ej2MaskedTextBox()
      .ej2NumericTextBox()
      .ej2Pager()
      .ej2ProgressButton()
      .ej2Radio()
      .ej2Slider()
      .ej2Switch()
      .ej2TextBox()
      .ej2TimePicker()
      .ej2TreeView()
      .ej2Uploader()
    return this;
  }

  useDefaults() {
    return this.useAll().useTheme("material");
  }

  useTheme(theme: "material" | "bootstrap" | "bootstrap4" | "fabric" | "highcontrast") {
    switch (theme) {
      case "material":
        return this.materialTheme();
      case "fabric":
        return this.fabricTheme();
      case "bootstrap":
        return this.bootstrapTheme();
      case "bootstrap4":
        return this.bootstrap4Theme();
      case "highcontrast":
        return this.highcontrastTheme();
      default:
        return this;
    }
  }

  materialTheme() {
    this.resources.push(PLATFORM.moduleName("@syncfusion/ej2/material.css"));
    return this;
  }

  fabricTheme() {
    this.resources.push(PLATFORM.moduleName("@syncfusion/ej2/fabric.css"));
    return this;
  }

  bootstrapTheme() {
    this.resources.push(PLATFORM.moduleName("@syncfusion/ej2/bootstrap.css"));
    return this;
  }

  bootstrap4Theme() {
    this.resources.push(PLATFORM.moduleName("@syncfusion/ej2/bootstrap4.css"));
    return this;
  }

  highcontrastTheme() {
    this.resources.push(PLATFORM.moduleName("@syncfusion/ej2/highcontrast.css"));
    return this;
  }

  withoutGlobalResources(): ConfigBuilder {
    this.useGlobalResources = false;
    return this;
  }

  ej2Accordion(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/accordion/ej2-accordion'));
    return this;
  }

  ej2AutoComplete(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/autoComplete/ej2-autoComplete'));
    return this;
  }

  ej2Button(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName("./widgets/button/ej2-button"));
    return this;
  }

  ej2Checkbox(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName("./widgets/checkbox/ej2-checkbox"));
    return this;
  }

  ej2CheckboxList(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName("./widgets/checkbox/ej2-checkbox-list"));
    return this;
  }

  ej2ComboBox(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/comboBox/ej2-comboBox'));
    return this;
  }

  ej2DashboardLayout(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/dashboardLayout/ej2-dashboardLayout'));
    return this;
  }

  ej2DatePicker(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/datePicker/ej2-DatePicker'));
    return this;
  }

  ej2DateRangePicker(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/dateRangePicker/ej2-dateRangePicker'));
    return this;
  }

  ej2DateTimePicker(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/dateTimePicker/ej2-dateTimePicker'));
    return this;
  }

  ej2DropDownList(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/dropDownList/ej2-dropDownList'));
    return this;
  }

  ej2Grid(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName("./widgets/grid/ej2-grid"));
    return this;
  }

  ej2ListView(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/listView/ej2-listView'));
    return this;
  }

  ej2MaskedTextBox(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/maskedTextBox/ej2-maskedTextBox'));
    return this;
  }

  ej2NumericTextBox(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/numericTextBox/ej2-numericTextBox'));
    return this;
  }

  ej2Pager(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/pager/ej2-pager'));
    return this;
  }

  ej2ProgressButton(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName("./widgets/progressButton/ej2-progress-button"));
    return this;
  }

  ej2Radio(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName("./widgets/radio/ej2-radio"));
    return this;
  }

  ej2Slider(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/slider/ej2-slider'));
    return this;
  }

  ej2Switch(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/switch/ej2-switch'));
    return this;
  }

  ej2TextBox(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName("./widgets/textBox/ej2-textBox"));
    return this;
  }

  ej2TimePicker(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/timePicker/ej2-timePicker'));
    return this;
  }

  ej2TreeView(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName('./widgets/treeView/ej2-treeView'));
    return this;
  }

  ej2Uploader(): ConfigBuilder {
    this.resources.push(PLATFORM.moduleName("./widgets/uploader/ej2-uploader"));
    return this;
  }
}

I am using this bridge in other projects.

1 Like

I found this, but no luck for me https://github.com/aurelia/webpack-plugin/issues/164

When I get some time, I think I’m going to try a regular CLI build. I can’t stand getting hung up on things like this. It cost me lots of time with this project and my electron experiments.

1 Like

Exactly my thinking, especially with electron where bundles don’t really matter where everything is collocated CLI bundler is just a dream. Also with Monaco needing AMD this was a perfect combo for my project

1 Like

I hit a road block with both RequireJS and system JS. Both I’m assuming are some simple configuration that I do not know about. Both are actually building successfully

RequireJS is actually complaining about not finding during run aurelia-bootstrapper.js. It is in the bundle, but things don’t seem to be point to the right location

SystemJS is probably something similar. systemjs is trying to load tslib.es6 and it cannot find it.

1 Like

Would anyone be willing to try the 2 packages I posted a few responses up? I hope us working through these problems it will help others.

1 Like

I did have a look at the two packages you listed but wasn’t sure what the issue was, what you are trying to achieve, how they are configured or what webpack config you’re using.

If you share a full repository which is resulting in the error I’d be happy to take a look.

1 Like

I can’t share a full repo at this point. I’d have to create a new project with the bulk of my real work stripped out. The issue is that webpack is not walking the plugins to pack up what the plugin is using. None of the views/css are being packed.

Those aren’t local packages, they are actual NPM packages that work fine in my non workspaces webpack project

This is the webpack config I’m using in the workspace project

import { AureliaPlugin } from 'aurelia-webpack-plugin';
import * as HtmlWebpackPlugin from 'html-webpack-plugin';
import { resolve } from 'path';

const mode = process.argv.includes('--production') ? 'production' : 'development';

export = (): object => {
    return {
        mode: mode,
        resolve: {
            extensions: ['.ts', '.js'],
            modules: ['src', 'node_modules', '../../node_modules'].map((p) => resolve(__dirname, '..', p)),
            symlinks: false
        },
        entry: { app: './src/main' },
        output: {
            filename: '[name].js'
        },
        watch: mode === 'development',
        devtool: mode === 'development' ? 'inline-source-map' : 'source-map',
        devServer: {
            contentBase: './dist'
        },
        optimization: {
            namedModules: true,
            concatenateModules: false
        },
        module: {
            rules: [
                {
                    test: /\.scss$/,
                    use: ['style-loader', 'css-loader', 'sass-loader'],
                    issuer: /\.[tj]s$/i
                },
                {
                    test: /\.scss$/,
                    use: ['css-loader', 'sass-loader'],
                    issuer: /\.html?$/i
                },
                {
                    test: /\.css$/i,
                    issuer: [{ not: [{ test: /\.html$/i }] }],
                    use: ['style-loader', 'css-loader']
                },
                {
                    test: /\.css$/,
                    use: ['css-loader'],
                    issuer: /\.html?$/i
                },
                {
                    test: /\.[tj]s$/i,
                    loader: 'ts-loader',
                    exclude: [/node_modules/, /\.spec\.[tj]s$/]
                },
                {
                    test: /\.html$/i,
                    use: [
                        {
                            loader: 'html-loader',
                            options: {
                                minimize: true
                            }
                        }
                    ]
                }
            ]
        },
        plugins: [
            new AureliaPlugin({ aureliaApp: undefined }),
            new HtmlWebpackPlugin({
                template: 'src/index.ejs',
                metadata: { dev: mode !== 'production' }
            })
        ]
    };
};

1 Like

@timfish I’ll have to create a simple project later with just the basics.

1 Like

I created a test project and I also moved my syncfusion-bridge code to github. I had to strip out all of my
“real” code, but the error was recreated with what is left. 2 of the packages don’t do much in the current form, but I wanted to leave the setup the same as my real work.

The control-set/ui-temp package is attempting to configure the syncfusion bridge plugin which in turn loads the syncfusion components. I am thinking that any plugin that is doing what my bridge plugin does may have the same problem.

I want to do another experiment to see if placing @view on the external plugin classes will do the trick as this seems like the same problem as above. Ultimately that is not a great approach as there may be other plugins that have the same issue. Thank you for any help you can offer

1 Like

@elitemike, I had a small amount of time to look through your repos.

Firstly, I just couldn’t get TypeScript to resolve modules across a workspace with versions like this:

 "@control-set/ui-temp": "*",

I appears that you have to use some valid version that matches the one in your package.json. I just do this for my private modules:

"@control-set/ui-temp": "0.0.0",

Secondly, as far as I know, the only valid use case of PLATFORM.moduleName with latest Aurelia is for the moduleId in the router. Instead you should just be using plain imports which Webapck or any other bundler will resolve. The less you rely on the Aurelia Webpack plugin the better.

So rather than:

config.globalResources([PLATFORM.moduleName('./myElement')]);

You should use regular imports:

import { MyElement } from './myElement';

// ...
config.globalResources([MyElement]);

And rather than:

import { useView, PLATFORM } from 'aurelia-framework';

@useView(PLATFORM.moduleName('@control-set/ui-temp/myElement.html'))
export class MyElement {
    public message = 'My Custom Element';
}

You should use:

import { customElement, inlineView } from 'aurelia-framework';
import template from './myElement.html';

@customElement('my-element')
@inlineView(template)
export class MyElement {
    public message = 'My Custom Element';
}

I went through and fixed these issues in the yarn-workspace-with-lerna repository and then got errors in aurelia-syncfusion-ej2-bridge.

1 Like

Thanks,

Oddly I had no issues other than the bridge trying to load syncfusion. Maybe I’m doing something completely wrong, but unless I used PLATFORM.moduleName webpack production builds fail due to module names. I once had all of my projects setup to not use moduleName then when time came to do a production build I found I had to update that.

I am hoping someone from the aurelia team can chime in on this

1 Like

This is because Webpack production builds minimise the JavaScript and this involves renaming classes to minimise bundle sizes. If classes are renamed, Aurelia can’t find them by conventions.

You can negate this by annotating your classes with @customElement('some-name'), @customAttribute, @valueConverter, etc, etc.

2 Likes

I’ll give it a go later. Thanks.

1 Like

SO I think the reason I differed from your test project is I can’t seem to get these imports to work. Typescript complains that it can’t find the module. This has to be a configuration issue, but I don’t see what is special about your tsconfigs.

I’d love to figure this out.

I think doing a `const template =require(‘’./file.html’) may work. I have to update everything and my kids are driving me crazy at the moment. I won’t know until all are updated in my real project

1 Like

I got a few components to work after making changes to the Syncfusion bridge. Thanks for your help on that. I have to change so many components to get this all to work, but this is a start.

I still haven’t figured out how to get import template from "./myComponenent.html

1 Like