alamode

3.7.1 • Public • Published

ÀLaMode

npm version Build status Node.js CI

ÀLaMode is a RegExp-based transpiler of source code in Node.JS that supports transpilation of import and export statements including source map for debugging, while keeping the original code pretty much the same (no interop require). It also can transpile JSX (without source maps ATM and some minor limitations).

The package can be used via the CLI to build packages, or via the require hook to transform modules on-the-fly.

If you've tried it and are having trouble seeing unknown keyword export, check if your issue falls under the category described in the troubleshooting. That's the single problem that we've seen after a year of using this software.

yarn add -D alamode
npm i --save-dev alamode

Why ÀLaMode

ÀLaMode is a neat, fast, low-weight alternative to AST-based transpilers. If you only want import and export statements, don't disrespect yourself by continuing to use Babel, and make a switch to ÀLaMode today. What am I talking about? Read next.

It's Neat

The source code is left pretty much intact, with line numbers preserved, and exports just being renamed to module.export while making sure to export the default module first if it is present. There is no need to come up with super-fancy solutions and try to build a rocket when all you need is a remote control. That means, don't worry about EcmaScript modules standard, it's significance is blown out of proportions by the "community" who has nothing better to do. Just rename exports to module.exports and imports to require — that's the philosophy behind ÀLaMode.

Source CodeTranspiled Code
import Stream, {
  Transform,
} from 'stream'
import { join } from 'path'
 
export default class S extends Transform {
  /**
   * Creates a new instance.
   * @param {string} path 
   * @param {Stream} [parent] 
   */
  constructor(path, parent) {
    super()
    this.source = join('example', path)
    if (parent instanceof Stream)
      this.pipe(parent)
  }
}
 
/**
 * A function that returns `c`.
 * @param {string} input 
 */
export const c = (input = '') => {
  return 'c' + input ? `-${input}` : ''
}
 
/**
 * A function that returns `b`.
 * @param {number} times 
 */
export const b = (times = 0) => {
  return 'b' + times ? `-${times}` : ''
}


const Stream = require('stream'); const {
  Transform,
} = Stream;
const { join } = require('path');
 
class S extends Transform {
  /**
   * Creates a new instance.
   * @param {string} path 
   * @param {Stream} [parent] 
   */
  constructor(path, parent) {
    super()
    this.source = join('example', path)
    if (parent instanceof Stream)
      this.pipe(parent)
  }
}
 
/**
 * A function that returns `c`.
 * @param {string} input 
 */
const c = (input = '') => {
  return 'c' + input ? `-${input}` : ''
}
 
/**
 * A function that returns `b`.
 * @param {number} times 
 */
const b = (times = 0) => {
  return 'b' + times ? `-${times}` : ''
}
 
module.exports = S
module.exports.c = c
module.exports.b = b
Show Babel Output
"use strict";
 
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.b = exports.c = exports.default = void 0;
 
var _stream = _interopRequireWildcard(require("stream"));
 
var _path = require("path");
 
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }
 
class S extends _stream.Transform {
  /**
   * Creates a new instance.
   * @param {string} path 
   * @param {Stream} [parent] 
   */
  constructor(path, parent) {
    super();
    this.source = (0, _path.join)('example', path);
    if (parent instanceof _stream.default) this.pipe(parent);
  }
 
}
/**
 * A function that returns `c`.
 * @param {string} input 
 */
 
 
exports.default = S;
 
const c = (input = '') => {
  return 'c' + input ? `-${input}` : '';
};
/**
 * A function that returns `b`.
 * @param {number} times 
 */
 
 
exports.c = c;
 
const b = (times = 0) => {
  return 'b' + times ? `-${times}` : '';
};
 
exports.b = b;

It Has 0 Dependencies

ÀLaMode does not install any additional dependencies, and it has been optimised with Google Closure Compiler. That means you don't have to wait ages for each new real dependency in your project to link with Babel's bloatware you don't care about. Just focus on the work and enjoy the single new directory in your node_modules.

ÀLaMode: 8 yarn.lock linesBabel: 1650 yarn.lock Lines
Installing ÀLaMode in 1 sec Linking Babel's Dependencies in 20 sec

It Respects JSDoc

Let's face it, Babel is software for masses that has capitalized on people's vulnerability in wanting to use import and export statements which is one of the best features on the language. You say let them be, I say look what they are doing to your documented code without caring a single bit:

ÀLaMode: Gold StandardBabel: JSDoc Enemy
Correct JSDoc With ÀLaMode Broken JSDoc With Babel

Original Source:

/**
 * A function that returns `b`.
 * @param {number} times 
 */
export const b = (times) => {
  return 'b' + (times ? `-${times}` : '')
}

See if you can figure out why this happens. Unlike Babel, our Node.JS Development Company cares about quality and developer experience, so we're offering ÀLaMode as a simple and lightweight solution to transpile packages.

Table Of Contents

Installation

The software can be installed either as a global dependency, or as a project dependency.

Global

When installed globally, it will be used directly via a binary, such as alamode src -o build.

Package Manager Installation
npm npm i -g alamode
yarn yarn add global alamode

Project

When installed in a project, it will be used via the package.json script, e.g., yarn build or npm run build.

{
  "name": "project",
  "version": "1.0.0",
  "description": "An example project",
  "main": "build",
  "scripts": {
    "build": "alamode src -o build"
  },
  "files": ["build"],
  "license": "MIT"
}
Package Manager Installation
npm npm i --save-dev alamode
yarn yarn add -DE alamode

CLI

The binary accepts a path to a single file, or a directory with the source code as the first argument, and a path to the build folder via -o argument.

alamode src -o build

There are other arguments which can be passed.

Argument Short Description
source The location of the input file or directory to transpile.
--output -o The location of where to save the transpiled output.
--ignore -i Comma-separated list of files inside of source dir to ignore, for example, bin,.eslintrc.
--noSourceMaps -s Disable source maps.
--extensions -e Files of what extensions to transpile. Default js,jsx.
--debug -d Will make ÀLaMode stop after replacing markers.
--require -r Renames require calls into imports, and module.exports assignments to exports. Great for refactoring older code.
--env The environment. Analogue to setting ALAMODE_ENV env variable.
--version -v Show the version number.
--help -h Display the usage information.

Additional JSX options are also available:

Argument Short Description
--jsx -j Enable JSX mode: only update JSX syntax to use hyperscript. Does not transpile import/export statements.
--module -m Works together with jsx to also transpile modules while transpiling JSX.
--preact -p When transpiling JSX, automatically insert at the top import { h } from "preact".
--preact-externs -E Same as preact, but imports from @externs/preact import { h } from "@externs/preact".

Setting the NODE_DEBUG environmental variable to alamode will print the list of processed files to the stderr.

$ NODE_DEBUG=alamode alamode src -o build -i bin/alamode.js
ALAMODE 97955: index.js
ALAMODE 97955: bin/catcher.js
ALAMODE 97955: bin/index.js
ALAMODE 97955: lib/index.js

--help

Shows all available commands.

ÀLaMode
A tool to transpile JavaScript packages using regular expressions.
Supports import/export and JSX transpilation.
https://artdecocode.com/alamode/

  alamode source [-o destination] [-i dir,file] [--env env] [-s]

    source            	The location of the input file or directory to transpile.
    --output, -o      	The location of where to save the transpiled output.
    --ignore, -i      	Comma-separated list of files inside of `source` dir to
                      	ignore, for example, `bin,.eslintrc`.
    --noSourceMaps, -s	Disable source maps.
    --extensions, -e  	Files of what extensions to transpile.
                      	Default: js,jsx.
    --debug, -d       	Will make ÀLaMode stop after replacing markers.
    --require, -r     	Renames `require` calls into imports, and `module.exports`
                      	assignments to exports.
                      	Great for refactoring older code.
    --env             	The environment. Analogue to setting `ALAMODE_ENV`
                      	env variable.
    --version, -v     	Show the version number.
    --help, -h        	Display the usage information.

  Example:

    alamode src -o build -s

JSX transpilation
Allows to transpile JSX using RegExes.

  alamode source [-o destination] -j [-mpE]

    --jsx, -j           	Enable JSX mode: only update JSX syntax to use hyperscript.
                        	Does not transpile `import/export` statements.
    --module, -m        	Works together with `jsx` to also transpile modules while
                        	transpiling JSX.
    --preact, -p        	When transpiling JSX, automatically insert at the top
                        	`import { h } from "preact"`.
    --preact-externs, -E	Same as `preact`, but imports from `@externs/preact`
                        	`import { h } from "@externs/preact"`.

  Example:

    alamode src -o build -j -m

Compiling JSX: --jsx, --preact

ÀLaMode can transpile JSX syntax. In the jsx mode, the import/export statements will be left intact, but the source code will be transformed to add pragma invocations, such as h(Component, { props }, children). The default pragma is h for Preact, and to avoid writing import { h } from 'preact' in each file, the -p option can be passed for ÀLaMode to add it automatically.

For example, the following file can be compiled:

import { render } from 'preact'
 
const Component = ({ test, ...props }) => (
  <div id="example" {...props}>
    {test}
  </div>
)
render(<Component cool>Example</Component>, document.body)

Using the alamode example/index.jsx -j -p command:

import { h } from 'preact'
import { render } from 'preact'
 
const Component = ({ test, ...props }) => (
   h('div',{...props,'id':"example"},
    test,
  )
)
render( h(Component,{cool:true},`Example`), document.body)

CSS Injector

ÀLaMode can transpile the import './style.css' directives into import './style.css.js', where style.css.js becomes a module with a css-injector script that will add your CSS to the head element.

ÀLaNode

To be able to spawn modules without having to create a proxy file as below:

require('alamode')()
require('./package')

ÀLaMode bundles a binary called alanode. It will do the same thing as above, so that running modules with import and export statements becomes really easy.

$ alanode source

It changes import and export statements into require calls and module.export expressions. It also normalises process.argv to hide its presence, so that programs can safely keep using the argv array without unexpected results.

With the following file that uses an import:

import { constants } from 'os'
import { basename } from 'path'
 
console.log(basename(process.argv[1]))
console.log(constants.signals.SIGINT)

$ alanode t will generate the result successfully:

t
2

ÀLaNode is also available as a standalone package alanode.
npm version

.alamoderc.json

A transform can support options which are set in the .alamoderc.json configuration file. The configuration file is read from the same directory where the program is executed (cwd). Options inside of the env directive will be active only when the ALAMODE_ENV environmental variable is set to the env key.

{
  "env": {
    "test-build": {
      "import": {
        "replacement": {
          "from": "^((../)+)src",
          "to": "$1build"
        }
      }
    }
  },
  "import": {
    "alamodeModules": ["alamode", "example"],
    "stdlib": {
      "path": "stdlib.js",
      "packages": ["example"]
    }
  }
}

Transforms

The main import and export transforms are included as part of ÀLaMode.

  • 📥@a-la/import Changes imports to requires. Read Wiki for additional options and guidance on how to test builds and build source code to use the standard library compiled out of all dependencies using Depack.
  • 📤@a-la/export Updates export to module.exports while preserving whitespace for human-readable output.

Require Hook

The purpose of the require hook is to be able to transpile files automatically when they are imported.

To use this feature, alamode needs to be required in a separate file, after which the import and export statements will become available.

For example, take the following directory structure, with a main and library files:

example/require
├── debug.js
├── index.js
├── lib.js
├── multiple.js
└── require.js
index.js lib.js
import getInfo from './lib'
 
console.log(getInfo())
import { platform, arch } from 'os'
 
export default () => {
  return `${platform()}:${arch()}`
}

The require hook would work in the following way:

require('alamode')()
require('.')

By executing the node require.js command, alamode will be installed and it will do its job dynamically for every .js file that is required, enabling to use import and export statements.

darwin:x64

_alamode.HookConfig: The options for ÀLaMode Hook.

Name Type Description Default
pragma string What pragma to add on top of JSX programs. Default const { h } = require('preact');. -
noWarning boolean Disable warnings when resetting existing hooks. false
ignoreNodeModules boolean Auto-ignore node_modules. Independent of any matcher. true
matcher (path: string) => boolean The function that will be called with the path and return whether the file should be transpiled. -

Multiple Calls To Alamode()

When the call is made multiple times in the program, the latter calls will revert the previous hooks and install new ones. The warning will be shown unless the noWarning option is set to true.

const alamode = require('alamode')
alamode()
 
// in other file
const path = require('path')
const preact = path.relative('', path
  .dirname(require.resolve('preact/package.json')))
alamode({
  pragma: `const { h } = require("./${preact}");`,
  // noWarning: true, // to disable output
})
Reverting JS hook to add new one.
Reverting JSX hook to add new one, pragma:
const { h } = require("./node_modules/preact");

This can happen when the tests are set up to run with Zoroaster with the -a flag for alamode, and the source code also tries to install the require hook.

Source Maps

The source maps are supported by this package, but implemented in a hack-ish way. The transforms will aim to preserve line and column numbers, so that no additional remapping is required. However this is not enough to generate a source map good enough for a debugger -- it needs to know about mappings of positions between segments which can be operators, function calls, etc. alamode simply breaks the source code into distinct chunks such as white-spaces, identifiers and comments, and down into individual symbols. Using this method, the size of a source map is larger, but it still works. In further versions, this will be improved to allow to extract real segments.

source map visualistion

Click to View: debug session
Alt: Debugging a source code with source maps.

Troubleshooting

Because there can be many intricacies when transpiling with regular expressions, problems might arise from time to time. If using the require hook, the best solution is to build the source code using alamode binary, and see where the error occurs. Then it must be analysed why it happens, for example:

  • The import or export transform does not match the case.
  • A portion of source code is cut out before the transform with markers so that the line does not participate in a transform.

So the single most common problem that we've experienced, is using the // and /* inside string literals (`), e.g.,

const host = 'localhost'
const port = 9999
const url = `https://${host}:${port}`
 
export const test = 'hello world'
 
const otherUrl = `https://${host}:${port}`
example/trouble.js:5
export const test = 'hello world'
^^^^^^

SyntaxError: Unexpected token export
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)
    at Module._compile (module.js:617:28)
    at Module.p._compile (node_modules/alamode/compile/depack.js:49:18)
    at Module._extensions..js (module.js:664:10)
    at Object.k.(anonymous function).y._extensions.(anonymous function) [as .js] (node_modules/alamode/compile/depack.js:51:7)
    at Module.load (module.js:566:32)
    at tryModuleLoad (module.js:506:12)
    at Function.Module._load (module.js:498:3)
    at Module.require (module.js:597:17)

This is because //${host}:${port}` will be cut until the end of the line as a comment prior to the template, and the template will match until the next opening backtick rather than the correct one, taking out the export from the transformation. To validate that, we can run the alamode src -d command:

const host = %%_RESTREAM_STRINGS_REPLACEMENT_0_%%
const port = 9999
const url = %%_RESTREAM_LITERALS_REPLACEMENT_0_%%https:%%_RESTREAM_INLINECOMMENTS_REPLACEMENT_1_%%

Now to fix this issue, either use ' to concatenate strings that have /* and //, or use import { format } from 'url' to dynamically create addresses.

Copyright & License

GNU Affero General Public License v3.0

You CAN use the require hook for your own code without publishing the source code. For example, you CAN run a web-server where your source code is transpiled on-the-fly. On the other hand, you're not allowed to build other software packages that link to ÀLaMode to enable transpilation feature for your users without adhering to Affero.

Affero is better for Open Source anyway, why don't you consider it instead of MIT.

Art Deco © Art Deco™ for À La Mode 2020

Package Sidebar

Install

npm i alamode

Weekly Downloads

54

Version

3.7.1

License

AGPL-3.0

Unpacked Size

451 kB

Total Files

28

Last publish

Collaborators

  • zvr