Time Travelling with Babel ⏰

jkone27
Travix Engineering
Published in
6 min readAug 23, 2022

Turning legacy js into modern ts and back

Babel is our Flux Capacitor!

This month we were able to achieve some 😻 happy transformation* in our FE services, which are basically aspnetcore apps serving static content in the wwwroot folder, and relying on some obsolete js framework (durandaljs) for view, routing and navigation.

We migrated our legacy AMD requirejs modules style .js files to new es6 .ts files, and Babel.js is responsible as well for uglifying them back to .js and requirejs for the final runtime.

diagram of the transformation

So it feels like taking a time machine, moving to the present from the past, and then going back to the past to deliver the awful javascript to the browser! And Babeljs is our Flux Capacitor making our old Delorean.js fly back and forth in Time!!!!

Our app runs on durandaljs (the ghost of aureliajs), an outdated js web framework, combinded with knockoutjs for viewmodels and view bindings.

durandaljs typical demo page and 200 AD browser

Unluckilly durandaljs is heavilly based on requirejs to work, at least from what we could figure out.

AMD requirejs modules logo is an arrow, to signify the pain it will inflict you in the end*

Why Move to ES6 Modules?

First and foremost, ES6 has modules baked in, and requirejs way of defining modules (Asynchronous Module Definition: AMD) is old and outdated.

Here is how it looks like at a high level, after configuring it:

//file.js
define(['dep1','dep2'], function(dep1,dep2) {
// .. use dep1 and dep2, but good luck with vscode intellisense
// if it's your own files/modules! ;)
return { .... } ; //export is possible ofc..})

One could say commonjs is the de-facto standard before ES6 modules, now are becoming more and more well-supported and largely adopted, due to having native commonjs support in node I suppose.

const dep1 = require('dep1'); // commonjs - nodejs style// exports also possible not relevant here

And here is instead how it looks in ES6, which, among other benefits is the default way of working with typescript and also is the easiest way to have easy-to-refactor, easy to test code (e.g. using jest) and to works well in vscode: in my opinion.

//file.ts
import dep1 from 'dep1'; //es6 default import..(many exists)
export default ... //es6 export (many exist)
babeljs home (our flux capacitor!)*

Past (and Future!)

Here the poor vscode has no clue over what’s going on, and whatever a “mapper” is, it will be a mr. nobody type(called any in typescript) forever and ever! That’s so sad and we have to do something about it 💩

requirejs past

Present (time travelling mode ON)

Intellisense and types from imports within your own modules as well as typings from libraries!

bright ts with es6 modules future

Use modern TS/JS features like unions or async await!

async await and union types

Nitty Gritty Details for the Bearded Man

  • Cleanup any dotnet created bundles .js files just to be on the safe side from wwwroot and rename to sources (they will be generated in the last step by dotnet build), and fix your .gitignore. 🐠

Ofc you always want to bundle as your last step, eventually you can replace it with webpack or some other tool in the future?

**/node_modules 
**/wwwroot
  • Add general babel.config.json configuration and install required dev dependencies (version might differ ofc), eventually add extra @types if needed from other scripts intellisense
    npm i -D… 🍏
"devDependencies": { 
"@babel/cli": "^7.18.6",
"@babel/core": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@types/durandal": "^1.1.3",
"@types/jquery": "^3.5.14",
"@types/knockout": "^3.4.72",
"@types/knockout.mapping": "^2.0.37",
"@types/knockout.validation": "^0.0.38",
"@types/toastr": "^2.1.39",
"babel-plugin-remove-use-strict": "^1.0.1",
"babel-plugin-transform-amd-to-es6": "^0.6.1",
"babel-plugin-transform-modules-simple-amd-with-default-exports": "^0.2.3",
"lodash": "^4.17.21",
"require-text": "^0.0.1",
"typescript": "^4.7.4"
}
  • Add a .babelrc.json in the directory of sources for the first transform 🐤
{
"plugins": [
"transform-amd-to-es6",
"remove-use-strict"
],
"ignore": [
"Scripts",
"App/Main.js" // contains more than 1 define...so just copy it
]
}
  • Run the first script, npm run transform-to-ts (after running if all is fine you can delete transform-to-ts..), 🍎 ⭐️ this will make out for most of the magic happening. Eventually… you might have to do some little manual fixes if your source javascript files were extremely awful to begin with 🐲 !!!!
"scripts": {    "transform-to-ts": "babel    src/Enosis.Configuration.WebService/sources --out-dir src/Enosis.Configuration.WebService/transformed --copy-files -x \".js\" --out-file-extension \".ts\"",    "transpile-ts-to-amd": "babel src/Enosis.Configuration.WebService/sources --out-dir src/Enosis.Configuration.WebService/wwwroot --copy-files -x \".ts\"" }

Delete js sources dir, rename transformed ts dir to sources and add general typescript tsconfig.json file, important>>> module resolution strategy is classic 🐵

{ 
"compilerOptions": {
"moduleResolution": "classic",
"esModuleInterop": true
}
}

Add a .babelrc.json in the sources folder, and run the second npm script..

"scripts": {   
"transpile-ts-to-amd": "babel src/Enosis.Configuration.WebService/sources --out-dir src/Enosis.Configuration.WebService/wwwroot --copy-files -x \".ts\"" }

important do not use builtins as that’s polifills with core-js, and we dont want them, else they will break our build for amd requirejs modules… also plugin-transform-typeof-symbol and remove-use-strict.

This is the most important step, first compiles .TS to .JS, then it adjusts the code a bit and turns all es6 modules into the legacy requirejs AMD modules in define format… 🐬

note: the sourcemaps option is essential for debugging in .ts, we need to inline it in the end of .js files

{ "presets": [ [  "@babel/preset-typescript" ], [        "@babel/preset-env", { 
"targets": ">99%",
"useBuiltIns": false,
"modules": false,
"forceAllTransforms" : false,
"exclude" : [ "@babel/plugin-transform-typeof-symbol" ] } ] ], "plugins": [
"babel-plugin-transform-modules-simple-amd-with-default-exports", "remove-use-strict" ],
"sourceMaps" : "inline" }

After running transpile-ts-to-amd, the final result should be something as follows, wwwroot is ignored by git, so it needs to be generated at each and every build!

After this you can still run dotnet build and have the bundles and minification for js scripts and css applied by dotnet..(with dontet packages making use of NUglify lib or WebOptimizer..) or bundle with whatever is your case.

Compiler Errors (or warnings?)

Now starts the fun with typescript! Compiler Errors: don’t make that face they are your best friends pal!

To improve the setup here you can trigger npn steps from .csproj file, and best to disable typescript automatic compilation for visualstudio and rider users as that would otherwise prevent build.

<Target Name="NpmCi" Inputs="../../package.json" Outputs="../../node_modules/.install-stamp" Condition="'$(SKIP_CSPROJ_NPM_TASKS)' != 'true'">
<Exec Command="npm ci" />
<!-- Write the stamp file, so incremental builds work -->
<Touch Files="../../node_modules/.install-stamp" AlwaysCreate="true" />
</Target>
<Target Name="TranspileTsToAmd" DependsOnTargets="NpmCi" BeforeTargets="BeforeBuild" Condition="'$(SKIP_CSPROJ_NPM_TASKS)' != 'true'">
<Exec Command="npm run transpile-ts-to-amd" />
</Target>
<PropertyGroup><TypeScriptCompileBlocked>true</TypeScriptCompileBlocked><TypeScriptCompileOnSaveEnabled>False</TypeScriptCompileOnSaveEnabled>
</PropertyGroup>

This way you can build everything locally with dotnet build, plus if you open a file with visualstudio instead of vscode, it will not complain but still build.

Note for Ci/Cd : in your dotnet build image you usually don’t have nodejs and npm installed, so you can either patch it with apt get or run an extra nodejs image as an extra transpile stage (that’s what we did and why i used the SKIP_CSPROJ_NPM_TASKS condition here.

The End

Now you can finally refactor that ugly javascript to more immutable typescript, and still get all the Typescript perks, like… async await!

A final note: some typings might not be correct or you might want to generate type inference from some local scripts, that ofc might also be possible, so as not to rely on external type definitions, but I wasn’t able to finalize it yet.

References and Notes

--

--