Structuring your app

There are a few good articles by Ashley Grant and Patrick Walters and Tiago Morais on structuring an Aurelia app but nothing I can find on the subject that is that recent. I have a reasonable sized app that’s over a year old that I’ve restructured a few times and am considering doing it again as I’m not happy with it.

root directory

Setup specific folders

|-- .github (Github templates)
|-- .vscode (VSCode only)
|-- aurelia_project (CLI only)
|-- custom_typings (Typescript only)

Ignored folders

|-- node_modules
|-- scripts
|-- assets (fonts, images, icons)
|-- doc (Changelog and other useful docs)
|-- locales (i18n translation files)
|-- src (the source code)
|-- test (just test setup)

My test folder is pretty empty as I prefer to have a __test__ folder in the same directory as each component.

Current source code structure

I’m not happy with the current structure as it doesn’t reflect the app. I can’t find out where I heard/read but it was something like your folder source should scream what your app does not how it’s built. The current structure does the latter.

|-- config (plugin config)
|-- controllers (will probably be dropped)
|-- features (encapsulated features)
|-- interfaces (typescript interfaces)
|-- models (models used by the views)
|-- private (the routes of the app)
|   |-- page 1
|   |-- page 2
|       |-- page 2a
|       |-- page 2b
|       |-- ...
|   |-- app-shell.css
|   |-- app-shell.html
|   |-- app-shell.ts
|   |- ...
|-- resources
|   |-- attributes
|   |-- dialogs
|   |-- elements
|   |-- forms (reusable forms)
|   |-- utilities
|   |-- value-converters
|   |-- events.ts (constants)
|   |-- index.ts
|   |-- key-codes.ts (more constants)
|-- services
|   |-- api
|       |-- service1.ts
|       |-- service2.ts
|       |-- ...
|   |-- log.ts
|-- styles
|   |-- base.css
|   |-- spacing.css
|   |-- ...
|-- environment.ts
|-- main.ts
|-- vars.css (css custom properties used through out the app)

It has a few problems

  • Too many top level folders
  • Folders with hardly anything in
  • Doesn’t show the features of the app

Version 2

Considering this instead to help focus on the features.

|-- app-shell
|   |-- app-shell.css
|   |-- app-shell.html
|   |-- app-shell.ts
|-- page 1
|-- page 2
|   |-- page 2a
|   |-- page 2b
|   |-- ...
|-- ...
|-- resources
|   |-- config (plugin config)
|   |-- controllers (will probably be dropped)
|   |-- features (encapsulated features)
|   |-- interfaces (typescript interfaces)
|   |-- models (models used by the views)
|   |-- attributes
|   |-- dialogs
|   |-- elements
|   |-- forms (resuable forms)
|   |-- services
|       |-- api
|           |-- service1.ts
|           |-- service2.ts
|           |-- ...
|       |-- log.ts
|   |-- utilities
|       |-- events.ts (constants)
|       |-- key-codes.ts (more constants)
|   |-- value-converters
|   |-- index.ts
|-- styles
|   |-- base.css
|   |-- spacing.css
|   |-- vars.css (css custom properties used through out the app)
|   |-- ...
|-- environment.ts
|-- main.ts

This flattens the private folder and puts the page/screens down into the src folder and everything else into resources (maybe lib would be better?). The resources folder is a little busier and probably still in need of tidying up.

For now there is a very little in the page folders other than the view and view-model pair but this might make it easier to restructure.

Bonus tip

You can set your paths in typescript so you don’t have to worry about relative paths. So your imports could be something like import { AccountService } from 'services/account-service' rather than import { AccountService } from '../../../../resources/services/account-service'

What do you think? What does yours look like? Is there anything you’re not happy with?

5 Likes

We’ve got a large Electron app which has been added to for over a year and we’re still going.

We went for something like version 2 where all the features have their own base directory. The idea being that all the features should be usable independently. This means the separate features often have their own models, services and elements which don’t spill into the other features.

Lately I’ve been looking to refactor into a mono-repository. This would help keep the features better segregated and pushes developers into creating a tree of dependencies rather than a spaghetti mess. This will also mean that if and when we want to ship our app in the browser or cordova, it will be easier to only include the components and features which are compatible.

Could you share relevant structure and build setup for electron?

Here is most of the build and Electron stuff.

We started with an aurelia jspm skeleton nearly two years ago. Over time it was converted to TypeScript and Gulp was replaced with npm scripts plus a few node scripts where that didn’t suffice.

  "scripts": {
    "postinstall": "electron-builder install-app-deps && jspm install -y",
    "start": "electron . --dev",
    "lint:ts": "tslint --config ./tslint.json --project tsconfig.json --force --fix",
    "lint:html": "node ./scripts/aurelia-lint.js",
    "lint": "npm-run-all lint:*",
    "clean": "del-cli ./dist/**",
    "build:ts": "tsc  -p ./tsconfig.json --pretty",
    "build:html": "html-minifier --input-dir ./src --output-dir ./dist --file-ext html --collapse-whitespace --conservative-collapse --remove-comments",
    "build:scss": "node-sass ./src -o ./dist -q --output-style compressed",
    "build": "run-p clean unbundle && run-p build:*",
    "watch:ts": "tsc --pretty --watch",
    "watch:html": "chokidar \"./src/**/*.html\" -c \"yarn run build:html\" --initial",
    "watch:scss": "chokidar \"./src/**/*.scss\" -c \"yarn run build:scss\" --initial",
    "watch": "run-p clean unbundle && run-p watch:*",
    "bundle": "yarn run build && node ./scripts/bundler.js --bundle",
    "unbundle": "node ./scripts/bundler.js --unbundle",
    "release": "yarn run bundle && build --config ./build/config.yml",
    "test": "xvfb-maybe karma start --single-run",
    "e2e": "cross-env TS_NODE_PROJECT=./e2e/tsconfig.json xvfb-maybe ava-ts ./e2e/src --serial --verbose"
  },

The Electron builds and tests run in CI on Windows, Mac and Linux only requiring yarn && yarn release using electron-builder. We include a github token in an environment variable on CI and electron-builder automatically puts the artifacts into Github releases on our private repository. It even takes care of building the half dozen native modules we use on each platform and in the required architectures.

electron-builder excludes almost everything by default so we can just define the things we want packaged in the app.

productName: App Name
appId: com.company.app-name
directories:
  # this contains icons, licence agreement, etc
  buildResources: "./build"
  output: "./releases"
files:
# include required root files
- "index.js"
- "index.html"
- "config.js"
# include all of static
- "static/**/*"
# Nothing in jspm_packages get packaged unless we define it here
- jspm_packages/system.js
# This doesn't get bundled so we have to include it
- jspm_packages/npm/pdfjs-dist@*/build/pdf.worker.js
# exclude dist apart from the bundles
- "!dist${/*}"
- dist/bundle-*.js
# electron-builder includes a pruned copy of node_modules 
# exclude all the build artifacts while ensuring we have the native module binary
- "!**/node_modules/*/build${/*}"
- "**/node_modules/*/build/**/*.node"
# exclude some other native artifacts
- "!**/node_modules/**/{*.cc,*.c,*.h,*.obj,*.pdb,*.sym,*.dsym}"
extraFiles:
# Copy the correct native hardware drivers for the platform
- from: build/${os}/${arch}
  to: resources
  filter:
  - "**/*"

After a release we manually push sourcemaps to Sentry.io so we get TypeScript code lines in our error reports and they get grouped nicely. The html-minifier options of --collapse-whitespace --conservative-collapse --remove-comments are specifically so our bundles are identical between builds on all 3 platforms. Without this we found the bundles and sourcemaps were slightly different which made the decoding and grouping break.

I’ve also recently helped work on sentry-electron which as well as capturing JavaScript exceptions also captures native crash minidumps, decodes the stacktraces and displays them with breadcrumbs telling you what the user was doing before the crash. We’re doing a lot of communication with native drivers so to have native error reporting has been great if only so I can prove to my customer that it’s their native code taking down the process!

1 Like

We went for the features approach where different zones (admin, sales, customer) but also validation have their own feature folder.
The book Learning Aurelia has a nice chapter about this topic “6 Design Concerns - Organizing and Decoupling”. It describes the pros and cons of multiple variants.

Having converted the project I’ve done the same, the resources folder is a bit lighter and I’m hoping to move it to a separate project, I haven’t looked a mono-respositories yet.

1 Like

The major downside to a mono-repository at the moment is that you can’t npm install/yarn add subdirectories in a git repository. So if you’re not publishing to npm and want to simply host in a private git repository, you’re out of luck unless you’re happy to use submodules.

@timfish hmm can you elaborate a bit. I was reading recently on yarn workspaces feature… and it promises golden mountains…

Both of these let you install npm packages from git repositories:

yarn add <git remote url>#<branch/commit/tag>
npm install <git repo url>[#<commit-ish> | #semver:<semver>]

Which is a great alternative to publishing to npm. This is especially helpful if you submit a PR for a library but want to use your changes before it gets merged and published on npm.

Now consider the default mono-repo directory layout where every library is in its own directory under a root packages:

├── package.json
├── packages
│   ├── my-company-resources
│   │   ├── package.json
│   │   ├── src
│   │   │   └── resource.ts
│   │   └── tsconfig.json
│   └── OurSuperApp
│       ├── package.json
│       ├── src
│       │   └── index.ts
│       └── tsconfig.json
├── tsconfig.base.json
└── yarn.lock

There is currently no way to tell yarn/npm to load a library from a git repository and look in a subdirectory for a package.json. They will currently only look in the root! This means it’s currently impossible to add any of the modules via git url.

I think the Aurelia libraries will be more accessible in a mono-repository but this issue is worth considering.

The npm team turned down the feature request in 2013, but it would only take yarn to implement it and npm would no doubt follow suit.

1 Like

I’ve been pondering app structure for some time now as I get a once in a job lifetime to redo my project.

I’m leaning more towards keeping pages in their own subdirectory under a single root folder (pages or areas) I personally don’t want pages to be at the root as I know i’ll have a lot of them.
pages
–page1
----subpage1
------subpage1.html
------subpage1.ts
----subpage2
----index.html
----index.ts
–page2

I plan on keeping reusable components in their own folder called components and each component will get it’s own subfolder.

I like to be able to have small bundles and my plan still allows that.

I’m a .Net developer and I tend to always have 2 DLLs in every project I work on. One is a common library that allows decoupled linking for all my features and the other is a core library. I’ve found similar success with Aurelia except I’ve tended to put my interfaces (typings) in the typings folder, but I think i’m going to change that as it’s just another folder on root that I don’t need.

In a nutshell, with keeping with the src folder that the CLI creates, i like to have minimal subfolders in src. Is it correct?? I have no idea, but it does work for me.

1 Like