JavaScript Preprocessor

Introduction

This is a simple JavaScript preprocessor to enable conditional compilation. The syntax of the preprocessor is a subset of the C preprocessor. This means that all JavaScript preprocessor directives are valid C preprocessor directives and can thus be used with the C preprocessor (but not the other way around.) The following directives are supported:

#define <identifier>

Defines an identifier.

#undef <identifier>

Removes a defined identifier, or does nothing it the identifier is not defined.

#ifdef <identifier>

Tests whether an identifier is defined, and parses the code block until either an #endif or #else directive is reached.

#ifndef <identifier>

Tests whether an identifier is not defined, and parses the code block until either an #endif or #else directive is reached.

#else

A code block executed when #ifdef or #ifndef return false. Code is parsed until an #endif directive is reached.

#endif

Ends a code block.

A simple example of some JavaScript code using conditional compilation:

#define DEBUG
#define SAFE_LOOP

function find(object, key) {
#ifdef SAFE_LOOP
  if (object.hasOwnProperty(key)) {
    return object[key];
  }
#else
  if (key in object) {
    return object[key];
  }
#endif
#ifdef DEBUG
  console.log('Warning: [find] returning undefined.');
#endif

  return undefined;
}

Note that the definitions do not necessarily need to be in the file, they can also be passed as arguments by your build tool of choice. This makes it easy to generate multiple versions of your project to―for example― create a debug build, or a specialized version designed to work in restrictive environments.

Running from Ant

Include the following code in your Ant build file, and ensure that the refid attribute points to a path reference that includes the Rhino JavaScript engine. You may also wish to change the src attribute of the scriptdef element to point to the location you have installed the JavaScript preprocessor.

<scriptdef name="preprocess" src="js-preprocess.js" language="javascript">
  <classpath>
    <path refid="js.lib"/>
  </classpath>
  <attribute name="defines" />
  <attribute name="todir" />
  <attribute name="file"/>
  <attribute name="tofile"/>
  <element name="fileset" type="fileset" />
</scriptdef>

Once you've done that, you can use the preprocessor to pre-process a set of files. Note that the preprocessor will create new files, so if you set your output directory to your input directory it will effectively strip the preprocessor directives from your source code.

<preprocess todir="target" defines="DEBUG">
  <fileset dir="src" includes="**/*.js" />
</preprocess>

You may also pre-process a single file using the file and tofile attributes.

<preprocess file="input.js" tofile="output.js" defines="DEBUG, SAFE_LOOP" />

You can pass multiple defines to the preprocessor by separating them with comma's.

Running from the command line using Rhino

You can also run the preprocessor from the command line using the Rhino JavaScript engine. The command line version takes the input file as the first argument and uses the remaining arguments as preprocessor definitions.

> rhino js-preprocess.js input.js [definition1 ...]

The preprocessor writes to the standard output, so to write it to a file you can redirect the standard output to a file as follows:

> rhino js-preprocess.js input.js DEBUG SAFE_LOOP > output.js

The example also shows how two definitions are passed to the preprocessor.

Using the preprocessor from JavaScript

The preprocessor exposes a single function called preprocess: you can simple call this function with as first argument a string (or an array of strings) containing the source code you want preprocessed. The second and optional argument is an object with as keys the predefined definitions you would like the preprocessor to use. The preprocessor will return to you an array of strings as the processed output. This output will have all preprocessor directives removed.

var result = preprocess('...', { 
  DEBUG: true,
  SAFE_LOOP: true
});

Frequently Asked Questions

Are there any plans to support #include directives?

Not at the present time. I think JavaScript would be better off with a proper module system, which controls dependencies, file inclusions, and packaging.

The JSLint/minification stage in my build process stops working because of the preprocessor directives.

The simple solution is to run the preprocessor before running JSLint or minifying your code. Alternatively, you could set up a filter in your build process that strips out all the preprocessor directives.