Lernajs/yarn workspaces with an aurelia application

(Update, I ended up working with yarn workspaces only, lernajs really isn’t needed for my application.)

I’m re-working some of my larger projects to be more modular. I want to use Lernajs in front of yarn workspaces. I’m using typescript as well. I’m curious to know how to get the most efficient workflow.

I will have a lot of packages that each application will rely on and I want those to reside in the “packages” folder. I will have the full blown Aurelia applications in another folder. Let’s call this folder examples. I’m using webpack projects that were created from the CLI.

If you make a change to one packages, since I’m using typsecript, it requires a rebuild of that module. How do i go about doing that, do i have to call build each time, or is there a better solution like some sort of watcher. Since each package needs to be built independently, i’m not sure what the best way to go about this is.

I know aurelia 2 is using lernajs and typsecript so I’m really curious to understand this.

1 Like

hi @elitemike, I’m doing this with a recent v1 project here: https://github.com/jbockle/au-jsonschema-form

for examples folder, there are a few slight tweaks required in the webpack.config.js:

  • resolve.modules - use absolute path to node_modules or use resolve.alias for aurelia packages
  • resolve.symlinks: false
  • resolve.alias your individual packages so you can use webpack dev server/hmr, requires splitting your tsconfig.json as well

There is likely other stuff I’m not thinking about right now, but feel free to ask any questions

2 Likes

Thank you so much. I think I am setup. I won’t know until I actual move this to my offline network and watch everything fail like it so often does :slight_smile:

lerna run build works, but lerna run build-watch doesn’t seem to be doing anything and I’m not getting any logging.

Also, what is the reason to have a cpx task in your build?

1 Like

I’m also starting with Lerna and in my case this Lerna project will be vanilla JS stuff, I also based my project a lot on the Aurelia 2 Lerna model. I had a similar question in Discourse couple weeks ago, in this post. I’m using the yarn workspace and I took you do also, but remember that it will create 1 main node_modules in the root, so you want to make sure that your Lerna project doesn’t mix and match too many stuff because you end up with a big node_modules, like in my case I was thinking to add Angular and Aurelia packages but end up not doing that because it’s not good practice and they don’t have anything common, instead I’ll keep 2 separate npm lib for Angular & Aurelia that will use the lerna one (which is all vanilla js).

All that to say that in my case, I have scripts in Lerna to do the Bootstrap (does all the symlink) then do a first TSC Build so that all other Packages have the code and the intellisense to work too. So in each package, I have a build script that does TSC Build and in the lerna package.json I call that build script with Lerna and by doing so it calls all the build. I then have dev:watch script that run in parallel, but that is where the biggest issue is, they run in parallel so because of that it is required to run the build at least once so all the code is ready and available by all other packages, then only after you can run the dev:watch script which like I said runs in parallel. For Unit Testing I use Jest and got that working too and even setup CircleCI, for the most part I took the config from my Aurelia-Slickgrid lib.

Another challenging thing I had was to get VSCode debugger to work, that was quite a task but eventually got it all working with one lauch.json file in the root (notice the sourceMapPathOverrides that will have to be updated every time you add a new package).

For reference, you can take a look at my slickgrid-universal lerna project. For now there are only 3 packages (2 of which that will later be used externally and 1 just for demo and troubleshooting purposes). I will add more packages in the future and once this project is all done and ready, I will rewrite Angular-Slickgrid and Aurelia-Slickgrid to use that core lerna project and have 1 main place to change code instead of having to change code in both framework when I do fixes currently.

Here’s also an article I found with Lerna and Yarn Workspace, it was written for a React project but still not bad to read.

2 Likes

Are either of you guys looking at yarn2 yet?

1 Like

sorry, build-watch was an old thing I forgot to remove, its no longer used. Just run the dev-app and webpack does everything required through the resolve.alias + tsconfig paths. cpx is just copying .html/.css files to dist on individual builds since tsc doesn’t touch them.

2 Likes

Are either of you guys looking at yarn2 yet?

not yet

1 Like

Okay that makes sense about cpx. I just the copy in webpack to do that. I"ll have to look at my webpack config, right now changes are not being seen unless I do a rebuild of the package.

1 Like

The problem with converting is forgetting to update some files. For whatever reason, my package.json of my webpack project was changed when I thought it should be correcct from a long time ago… I should have started out simple instead of trying to convert a large project right off the start

1 Like

I thought was making good progress. In my UI project I’m loading a package like a plugin.

aurelia.use.plugin(PLATFORM.moduleName('@control-set/ui'));
This package builds just fine, but when the web app is configuring the plugin, I’m getting Unable to find module with ID: @control-set/ui/{any html file}.html error

My index.ts file for the UI package is as follows. All paths are valid here

import { PLATFORM } from 'aurelia-pal';
import { FrameworkConfiguration } from 'aurelia-framework';

export function configure(config: FrameworkConfiguration): void {
    config.globalResources([
        PLATFORM.moduleName('./control-renderer'),
        PLATFORM.moduleName('./elements/description'),
        PLATFORM.moduleName('./elements/help'),
        PLATFORM.moduleName('./elements/validationError'),
        PLATFORM.moduleName('./elements/validationGutterError'),
        PLATFORM.moduleName('./elements/scrollspy/scrollspy'),
        PLATFORM.moduleName('./elements/scrollspy/scrollspy-item'),
        PLATFORM.moduleName('./elements/vertical_control_wrapper'),
        PLATFORM.moduleName('./elements/control_snapshot_wrapper'),
        PLATFORM.moduleName('./elements/listbox/listBox'),
        PLATFORM.moduleName('./elements/inlineView'),
        PLATFORM.moduleName('./elements/keysValueConverter')
    ]);

    config.aurelia.use.plugin(PLATFORM.moduleName('@control-set/factory'));
}

Here is my webpack.config

/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable prettier/prettier */
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');
const { AureliaPlugin, ModuleDependenciesPlugin } = require('aurelia-webpack-plugin');
const { ProvidePlugin } = require('webpack');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const CleanWebpackPlugin = require('clean-webpack-plugin');

// config helpers:
const ensureArray = (config) => config && (Array.isArray(config) ? config : [config]) || [];
const when = (condition, config, negativeConfig) =>
  condition ? ensureArray(config) : ensureArray(negativeConfig);

// primary config:

//console.log("dirname", path.resolve(`../../`, 'node_modules'))
const title = 'Aurelia Navigation Skeleton';
const outDir = path.resolve(__dirname, "dist");
const srcDir = path.resolve(__dirname, 'src');
const nodeModulesDir = path.resolve('../../', 'node_modules');
const baseUrl = '/';

const cssRules = [
  { loader: 'css-loader' },
];


module.exports = ({ production } = {}, { extractCss, analyze, tests, hmr, port, host } = {}) => ({
  resolve: {
    extensions: ['.ts', '.js'],
    modules: [srcDir, 'node_modules'],
    // Enforce single aurelia-binding, to avoid v1/v2 duplication due to
    // out-of-date dependencies on 3rd party aurelia plugins
    alias: { 'aurelia-binding': path.resolve(nodeModulesDir, 'aurelia-binding'),
            '@control-set/factory': path.resolve('../../packages/control-set-factory/src' ),
            '@control-set/common': path.resolve('../../packages/control-set-common/src'),
            '@control-set/ui': path.resolve('../../packages/control-set-ui/src')
        },
            
    symlinks: false
  },
  entry: {
    app: ['aurelia-bootstrapper']
  },
  mode: production ? 'production' : 'development',
  output: {
    path: outDir,
    publicPath: baseUrl,
    filename: production ? '[name].[chunkhash].bundle.js' : '[name].[hash].bundle.js',
    sourceMapFilename: production ? '[name].[chunkhash].bundle.map' : '[name].[hash].bundle.map',
    chunkFilename: production ? '[name].[chunkhash].chunk.js' : '[name].[hash].chunk.js'
  },
  optimization: {
    runtimeChunk: true,  // separates the runtime chunk, required for long term cacheability
    // moduleIds is the replacement for HashedModuleIdsPlugin and NamedModulesPlugin deprecated in https://github.com/webpack/webpack/releases/tag/v4.16.0
    // changes module id's to use hashes be based on the relative path of the module, required for long term cacheability
    moduleIds: 'hashed',
    // Use splitChunks to breakdown the App/Aurelia bundle down into smaller chunks
    // https://webpack.js.org/plugins/split-chunks-plugin/
    splitChunks: {
      hidePathInfo: true, // prevents the path from being used in the filename when using maxSize
      chunks: "initial",
      // sizes are compared against source before minification
      maxSize: 200000, // splits chunks if bigger than 200k, adjust as required (maxSize added in webpack v4.15)
      cacheGroups: {
        default: false, // Disable the built-in groups default & vendors (vendors is redefined below)
        // You can insert additional cacheGroup entries here if you want to split out specific modules
        // This is required in order to split out vendor css from the app css when using --extractCss
        // For example to separate font-awesome and bootstrap:
        // fontawesome: { // separates font-awesome css from the app css (font-awesome is only css/fonts)
        //   name: 'vendor.font-awesome',
        //   test:  /[\\/]node_modules[\\/]font-awesome[\\/]/,
        //   priority: 100,
        //   enforce: true
        // },
        // bootstrap: { // separates bootstrap js from vendors and also bootstrap css from app css
        //   name: 'vendor.font-awesome',
        //   test:  /[\\/]node_modules[\\/]bootstrap[\\/]/,
        //   priority: 90,
        //   enforce: true
        // },

        // This is the HTTP/1.1 optimised cacheGroup configuration
        vendors: { // picks up everything from node_modules as long as the sum of node modules is larger than minSize
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 19,
          enforce: true, // causes maxInitialRequests to be ignored, minSize still respected if specified in cacheGroup
          minSize: 30000 // use the default minSize
        },
        vendorsAsync: { // vendors async chunk, remaining asynchronously used node modules as single chunk file
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors.async',
          chunks: 'async',
          priority: 9,
          reuseExistingChunk: true,
          minSize: 10000  // use smaller minSize to avoid too much potential bundle bloat due to module duplication.
        },
        commonsAsync: { // commons async chunk, remaining asynchronously used modules as single chunk file
          name: 'commons.async',
          minChunks: 2, // Minimum number of chunks that must share a module before splitting
          chunks: 'async',
          priority: 0,
          reuseExistingChunk: true,
          minSize: 10000  // use smaller minSize to avoid too much potential bundle bloat due to module duplication.
        }
      }
    }
  },
  performance: { hints: false },
  devServer: {
    contentBase: outDir,
    // serve index.html for all 404 (required for push-state)
    historyApiFallback: true,
    hot: hmr || true,
    port: port || 8080,
    host: host
  },
  devtool: production ? 'nosources-source-map' : 'cheap-module-eval-source-map',
  module: {
    rules: [
      // CSS required in JS/TS files should use the style-loader that auto-injects it into the website
      // only when the issuer is a .js/.ts file, so the loaders are not applied inside html templates
      {
        test: /\.css$/i,
        issuer: [{ not: [{ test: /\.html$/i }] }],
        use: extractCss ? [{
          loader: MiniCssExtractPlugin.loader
        }, ...cssRules
        ] : ['style-loader', ...cssRules]
      },
      {
        test: /\.css$/i,
        issuer: [{ test: /\.html$/i }],
        // CSS required in templates cannot be extracted safely
        // because Aurelia would try to require it again in runtime
        use: cssRules
      },
      { test: /\.html$/i, loader: 'html-loader' },
      { test: /\.ts$/, loader: "ts-loader" },
      // embed small images and fonts as Data Urls and larger ones as files:
      { test: /\.(png|gif|jpg|cur)$/i, loader: 'url-loader', options: { limit: 8192 } },
      { test: /\.woff2(\?v=[0-9]\.[0-9]\.[0-9])?$/i, loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff2' } },
      { test: /\.woff(\?v=[0-9]\.[0-9]\.[0-9])?$/i, loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff' } },
      // load these fonts normally, as files:
      { test: /\.(ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/i, loader: 'file-loader' },
      {
        test: /environment\.json$/i, use: [
          { loader: "app-settings-loader", options: { env: production ? 'production' : 'development' } },
        ]
      },
      ...when(tests, {
        test: /\.[jt]s$/i, loader: 'istanbul-instrumenter-loader',
        include: srcDir, exclude: [/\.(spec|test)\.[jt]s$/i],
        enforce: 'post', options: { esModules: true },
      })
    ]
  },
  plugins: [
    ...when(!tests, new DuplicatePackageCheckerPlugin()),
    new AureliaPlugin(),
    new ProvidePlugin({
      'Promise': ['promise-polyfill', 'default']
    }),
    new ModuleDependenciesPlugin({
      'aurelia-testing': ['./compile-spy', './view-spy']
    }),
    new HtmlWebpackPlugin({
      template: 'index.ejs',
      metadata: {
        // available in index.ejs //
        title, baseUrl
      }
    }),
    // ref: https://webpack.js.org/plugins/mini-css-extract-plugin/
    ...when(extractCss, new MiniCssExtractPlugin({ // updated to match the naming conventions for the js files
      filename: production ? 'css/[name].[contenthash].bundle.css' : 'css/[name].[hash].bundle.css',
      chunkFilename: production ? 'css/[name].[contenthash].chunk.css' : 'css/[name].[hash].chunk.css'
    })),
    ...when(!tests, new CopyWebpackPlugin([
      { from: 'static', to: outDir, ignore: ['.*'] }])), // ignore dot (hidden) files
    ...when(analyze, new BundleAnalyzerPlugin()),
    /**
     * Note that the usage of following plugin cleans the webpack output directory before build.
     * In case you want to generate any file in the output path as a part of pre-build step, this plugin will likely
     * remove those before the webpack build. In that case consider disabling the plugin, and instead use something like
     * `del` (https://www.npmjs.com/package/del), or `rimraf` (https://www.npmjs.com/package/rimraf).
     */
    new CleanWebpackPlugin()
  ]
});

I followed the debugging modules write up with no luck

1 Like

I was having a similar issue, which is why I’m using variables throughout, which isn’t ideal. There is a built in way with aurelia webpack plugin to load views for node_modules using convention, by adjusting this option described here https://github.com/aurelia/webpack-plugin/wiki/AureliaPlugin-options#viewfor-and-viewextensions, however I was unsuccessful with that. @bigopon/@huochunpeng do you know how we can make this work within a lerna monorepo with tsconfig paths/webpack aliasing?

1 Like

p.s. I also tried @useView(PLATFORM.moduleName(’./view.html’)) which is recommended for plugins, without success

1 Like

Well I feel better that I’m not the only one that failed :slight_smile: I hope there is a better solution than what you found that works.

Have you tried to run your application as a production build? I ran into issues by not using moduleName in my plugins before when running as production

1 Like

@ghiscoding if you haven’t tried an aurelia application yet, it might be a good time before you get too far like I did.

1 Like

no prod builds yet - this is still wip for me.

1 Like

I was able to get @useView working if I used the full path with the would be plugin’s name, relatives path is still not working:
@useView(PLATFORM.moduleName('@au-jsonschema-form/core/components/schema-form.html'))

2 Likes

I can live with that if I must for now

1 Like

Good to know, however I still need the vanilla flavor anyway so I’ll keep going with it. I’m also assuming you guys will come up with a resolution before I even start adding the Aurelia part :wink:

Thanks for letting me know, I’m following your progress

2 Likes

I confirmed useView works in my case as well. It’s not the end of the world, but I would rather not do it.

1 Like

I seem to be running into view loading issues with an external plugin as well :confused: I’m back to wondering if webpack is worth the trouble

1 Like