I've implemented CommonJS Modules 1.1.1 as a simple bash script with a bit of help from M4.
I'm interested in comments on the shell scripting and the javascript output.
Some specific questions on bash scripting:
- How are backquotes different from $( ... ) as far as evaluating stuff in strings?
- Are there places where I have improperly quoted variables, where things could break
if path names have spaces or weird characters?
normalize_path
looks wrong to me, but I'm not sure how to correct it. - Is there a better way to increment/decrement variables?
- Is anything done in a bad style?
I'm also interested in comments on the javascript output (the anonymous bootstrap object, and its init
function).
Also, is this a good implementation of CommonJS Modules? It passes all of the unit tests, but it may still have flaws. One thing on my to-do list is adding a command line argument / environment variable for a list of include paths.
com4 - the main program.
#!/bin/bash
export program="CoM4"
export usage="Usage: $0 -i <in_file.js> -o <out_file.js>";
export com4_path="$( dirname $( readlink -f "$0" ))"
export included_files="included_files.tmp"
export tmp_path="_tmp"
export abs_path=""
export rel_path=""
export include=""
export in_file=""
export out_file=""
export fail=0;
export test_mode=0;
export html=0;
export var_global="window"
export var_main=""
. "$com4_path/include.sh"
get_options "$@"
mkdir "$tmp_path"
echo -n > "$out_file"
echo -n > "$included_files"
abs_path="`normalize_path $( dirname "$in_file" )`";
rel_path="$abs_path";
id="`basename "$in_file"`";
if [ $html = 1 ]; then
echo "<html><head><script>" >> "$out_file";
fi
if [ $test_mode = 1 ]; then
echo "function print(text){ console.log(text); }" >> "$out_file";
fi
if [ "$var_main" != "" ]; then
echo -n "$var_main = " >> "$out_file";
fi
echo "({modules:[ ">> "$out_file";
write_module "$id" 1 >> "$out_file";
for file in $( ls $tmp_path ); do
cat "$tmp_path/$file" >> "$out_file";
done
echo "
//
// $program bootstrap
//
0],init:function(){
var boot=this, exports=[], require=function(id){ return exports[id] || void boot.modules[id](require, id ? {id:id} : require.main, exports[id]={}) || exports[id]; };
require.main={id:0};
return require(0);
}}).init();" >> "$out_file";
if [ $html = 1 ]; then
echo "</script></head><body></body></html>" >> "$out_file";
fi
rm "$included_files"
rm -r "$tmp_path"
include.sh - helper functions.
get_options ()
{
while getopts "i:o:g:m:I:th" opt; do
case "$opt" in
i)
in_file="$OPTARG";;
o)
out_file="$OPTARG";;
g)
var_global="$OPTARG";;
I)
include="$OPTARG";; # TODO: -I / include dirs
t)
test_mode=1;;
h)
html=1;;
m)
var_main="$OPTARG";;
[?])
fail=1;;
esac
done
if [ "$in_file" = "" ]; then
echo >&2 "$0: No input file specified."
fail=1
fi
if [ "$out_file" = "" ]; then
echo >&2 "$0: No output file specified."
fail=1
fi
if [ $fail != 0 ]; then
echo >&2 "$usage"
exit $fail
fi
}
normalize_path ()
{
echo "$(cd $(dirname "$1"); echo $PWD/$(basename "$1"))"
}
write_module ()
{
if [ "`echo $1 | egrep '^\.\.?/'`" = "" ]; then
path=$(normalize_path "$abs_path/$1.js")
else
path=$(normalize_path "$rel_path/$1.js")
fi
module_name=`abs_to_rel "$abs_path" "$path"`
loaded=0
old_path="$rel_path"
rel_path=$( dirname "$path" )
count=0
if [ $2 ]; then
require="require";
else
require="[_require_]";
fi
for included in `cat $included_files`; do
count=$(( $count + 1 ))
if [ "$included" = "$path" ]; then
count=$(( $count - 1 ))
echo -n "$require($count)"
return
fi
done
tmp_file="$tmp_path/$count"
if [ "$2" = "" ]; then
echo -n "$require($count)"
fi
echo -n > "$tmp_file";
echo "$path" >> "$included_files"
echo "
//
// $module_name
//" >> "$tmp_file"
if [ -e "$path" ]; then
echo "function(require, module, exports){ " >> "$tmp_file"
m4 -P "$com4_path/macros.m4" "$path" >> "$tmp_file"
echo "}," >> "$tmp_file"
else
echo ",// Module \"$module_name\" not found." >> "$tmp_file"
echo "Warning: Module \"$module_name\" not found." >&2
fi
rel_path="$old_path"
}
# both $1 and $2 are absolute paths
# returns $2 relative to $1
abs_to_rel ()
{
source=$1
target=$2
common_part=$source
back=
while [ "${target#$common_part}" = "${target}" ]; do
common_part=$(dirname $common_part)
back="../${back}"
done
echo ${back}${target#$common_part/}
}
macros.m4 - require
macro.
m4_changequote([_,_])m4_dnl
m4_define(require, [_m4_esyscmd(
if [ $1 ]; then
. "$com4_path/include.sh"
write_module $1
else
echo -n "[_[_require_]_]"
fi
)_])m4_dnl
m4_define(GLOBAL, [_m4_esyscmd(
echo -n "$var_global"
)_])m4_dnl
Here is an example of the output from one of the unit tests.
The print
function is a debug-mode feature to make CommonJS unit tests work properly.
function print(text){ console.log(text); }
({modules:[
//
// ../program.js
//
function(require, module, exports){
var test = require(1);
var a = require(3);
var b = require(4);
test.assert(a.a, 'a exists');
test.assert(b.b, 'b exists')
test.assert(a.a().b === b.b, 'a gets b');
test.assert(b.b().a === a.a, 'b gets a');
test.print('DONE', 'info');
},
//
// ../test.js
//
function(require, module, exports){
exports.print = typeof print !== "undefined" ? print : function () {
var system = require(2);
var stdio = system.stdio;
stdio.print.apply(stdio, arguments);
};
exports.assert = function (guard, message) {
if (guard) {
exports.print('PASS ' + message, 'pass');
} else {
exports.print('FAIL ' + message, 'fail');
}
};
},
//
// ../system.js
//
,// Module "../system.js" not found.
//
// ../a.js
//
function(require, module, exports){
exports.a = function () {
return b;
};
var b = require(4);
},
//
// ../b.js
//
function(require, module, exports){
var a = require(3);
exports.b = function () {
return a;
};
},
//
// CoM4 bootstrap
//
0],init:function(){
var boot=this, exports=[], require=function(id){ return exports[id] || void boot.modules[id](require, id ? {id:id} : require.main, exports[id]={}) || exports[id]; };
require.main={id:0};
return require(0);
}}).init();