.js.rst
File Extension¶Intercept Node module loading for filenames ending in .js.rst
or
.json.rst
. This allows .js.rst
and .json.rst
files to be passed to
require
calls without including the suffix.
Implementation is copied from register.coffee in CoffeeScript and is similar to babel-register.
The implementation uses the require.extensions
global in Node. Setting a
key in this dictionary adds Node handling for new file extensions. The key is
the file suffix string to support, the value is a function to call to load and
process the file.
But require.extenions
has been deprecated in Node because there is the
potential for conflicts when a suffix is used by two modules simultaneously.
It also encourages the offloading of compilation to runtime instead of build
time.
Strictly speaking it should not be used. However:
.js.rst
and .json.rst
suffixes.require.extensions
implementation is
locked so Node is committed to keeping the current form indefinitely..js.rst
and
.json.rst
sources.For .js.rst
loads, read the file contents and use the parser defined in
index.js.rst
to extract the JavaScript codeblocks from the reStructured
Text. This material is passed to module._compile
for compilation and
incorporation into the running environment.
const literacy = require('./index.js');
function loadJsRstFile(module, filename) {
const javaScript = literacy.fromFile(filename).content;
module._compile(javaScript, filename);
}
Add this .js.rst
file extension handler to require.extensions
.
if (require.extensions) {
require.extensions['.js.rst'] = loadJsRstFile;
}
Similarly for .json.rst
files except parse the JSON content instead of
passing it to module._compile
.
function loadJsonRstFile(module, filename) {
const json = literacy.fromFile(filename).content;
module.exports = JSON.parse(json);
}
if (require.extensions) {
require.extensions['.json.rst'] = loadJsonRstFile;
}
This is not enough unfortunately since although the .js.rst
and
.json.rst
extensions are registered, the Node module loader isn’t able to
handle multi-part extensions. For this, we follow CoffeeScript in reimplementing
Module::load to support multi-part extension handling.
“This is a horrible thing that should not be required.”
Start by determining the best extension choice from the filename where best is the file extension with the most parts.
There is also the edge case of dotfiles to handle, i.e. a leading dot in the filename is legitimate.
Note although there is only one multi-part extension, we cannot simply test for it and then use the existing single-part extension implementation in Node for everything else. This is because other modules may have added multi-part extension implementations too. A specific test approach here would break these other modules.
This implementation follows the CoffeeScript directly.
const path = require('path');
const Module = require('module');
function findExtension(filename) {
const extensions = path.basename(filename).split('.');
Remove the initial dot from dotfiles. This means there can be filenames
consisting entirely of an extension, e.g. the .js.rst
case is handled by
trimming the initial .
and then putting it back before the test into
Module._extensions[...]
.
if (extensions[0] === '') {
extensions.shift();
}
Start with the longest possible extension and work towards the shortest.
while (extensions.shift()) {
const current = `.${ extensions.join('.') }`;
if (Module._extensions[current]) {
return current;
}
}
Default to the ‘.js’ file handler if nothing more specific is registered.
return '.js';
}
Now we have findExtension
, finish by patching the module load itself.
This involves adding the file directory path to the Node module paths list
and calling the extension handler for the file.
See the Node implementation of Module.prototype.load for comparison. It is
identical save for the findExtension
amendment.
const assert = require('assert').ok;
const debug = Module._debug;
if (require.extensions) {
Module.prototype.load = function load(filename) {
debug('load %j for module %j', filename, this.id);
assert(!this.loaded);
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));
const extension = findExtension(filename);
Module._extensions[extension](this, filename);
this.loaded = true;
};
}