alloy
Version:
TiDev Titanium MVC Framework
244 lines (222 loc) • 8.71 kB
JavaScript
/*
Sets options and wraps some functionality around the source mapping functions
provided by the UglifyJS library
*/
var SM = require('source-map'),
fs = require('fs-extra'),
path = require('path'),
U = require('../../utils'),
CONST = require('../../common/constants'),
babylon = require('@babel/parser'),
babel = require('@babel/core'),
logger = require('../../logger'),
_ = require('lodash');
var lineSplitter = /(?:\r\n|\r|\n)/;
// Try to match this in our babel.transformFromAst calls?
exports.OPTIONS_OUTPUT = {
ast: false,
// do NOT minify Alloy code because Titanium will do it!
minified: false,
compact: false,
comments: true,
babelrc: false,
passPerPreset: false,
retainLines: true
};
function mapLine(mapper, theMap, genMap, line) {
mapper.addMapping({
original: {
line: theMap.count++,
column: 0
},
generated: {
line: genMap.count++,
column: 0
},
source: theMap.filename
});
genMap.code += line + '\n';
}
function getTextFromGenerator(content, template) {
if (typeof content !== 'undefined' && content !== null) {
return content;
} else {
if (template && fs.existsSync(template)) {
return fs.readFileSync(template, 'utf8');
} else {
return '';
}
}
}
exports.generateCodeAndSourceMap = function(generator, compileConfig) {
var target = generator.target;
var data = generator.data;
var outfile = target.filepath;
var relativeOutfile = path.relative(compileConfig.dir.project, outfile);
var markers = _.map(data, function(v, k) { return k; });
var mapper = new SM.SourceMapGenerator({
file: path.join(compileConfig.dir.project, relativeOutfile),
sourceRoot: compileConfig.dir.project
});
// try to lookup the filename, falling back to the output file if we can't determine it
let filename;
if (data.__MAPMARKER_CONTROLLER_CODE__ && data.__MAPMARKER_CONTROLLER_CODE__.filename) {
filename = data.__MAPMARKER_CONTROLLER_CODE__.filename;
} else if (data.__MAPMARKER_ALLOY_JS__ && data.__MAPMARKER_ALLOY_JS__.filename) {
filename = data.__MAPMARKER_ALLOY_JS__.filename;
} else if (data.__MAPMARKER_NONCONTROLLER__ && data.__MAPMARKER_NONCONTROLLER__.filename) {
filename = data.__MAPMARKER_NONCONTROLLER__.filename;
} else {
filename = target.filename;
}
// the line counter and code string for the generated file
var genMap = {
count: 1,
code: ''
};
// The line counter and reported source name for the input template
var templateMap = {
count: 1,
filename: target.template || 'template.js'
};
// ensure target.templateContent will have a value so we can embed in sourcesContent
target.templateContent = getTextFromGenerator(target.templateContent, target.template);
target.lines = target.templateContent.split(lineSplitter);
_.each(markers, function(m) {
var marker = data[m];
// set the line counter for each "data" object we place into the generated code at certain point in template
// already has a filename, fileContent property
marker.count = 1;
// ensure marker.fileContent will have a value so we can embed in sourcesContent
marker.fileContent = getTextFromGenerator(marker.fileContent, marker.filepath);
marker.lines = marker.fileContent.split(lineSplitter);
});
// generate the source map and composite code
_.each(target.lines, function(line) {
var trimmed = U.trim(line);
if (_.includes(markers, trimmed)) {
templateMap.count++; // skip this line in the template count now or else we'll be off by one from here on out
_.each(data[trimmed].lines, function(line) {
mapLine(mapper, data[trimmed], genMap, line);
});
} else {
mapLine(mapper, templateMap, genMap, line);
}
});
// parse composite code into an AST
var ast;
try {
ast = babylon.parse(genMap.code, {
sourceFilename: outfile,
sourceType: 'unambiguous',
allowReturnOutsideFunction: true
});
} catch (e) {
let filename;
if (data.__MAPMARKER_CONTROLLER_CODE__) {
filename = data.__MAPMARKER_CONTROLLER_CODE__.filename;
} else if (data.__MAPMARKER_ALLOY_JS__) {
filename = data.__MAPMARKER_ALLOY_JS__.filename;
}
U.dieWithCodeFrame(`Error parsing code in ${filename}. ${e.message}`, e.loc, genMap.code);
}
// create source map and generated code
var options = _.extend(_.clone(exports.OPTIONS_OUTPUT), {
plugins: [
[require('./ast/builtins-plugin'), compileConfig],
[require('./ast/optimizer-plugin'), compileConfig.alloyConfig]
],
filename
});
if (compileConfig.sourcemap) {
// Tell babel to retain the lines so they stay correct (columns go wacky, but OH WELL)
// we produce our own source maps and we want the lines to stay as we mapped them
options.retainLines = true;
}
var outputResult = babel.transformFromAstSync(ast, genMap.code, options);
// produce the source map and embed the original source (so the template source can be passed along)
const sourceMap = mapper.toJSON();
sourceMap.sourcesContent = [ target.templateContent, data[markers[0]].fileContent ];
// append pointer to the source map to the generated code
outputResult.code += `\n//# sourceMappingURL=file://${compileConfig.dir.project}/${CONST.DIR.MAP}/${relativeOutfile}.${CONST.FILE_EXT.MAP}`;
// write the generated controller code
fs.mkdirpSync(path.dirname(outfile));
fs.writeFileSync(outfile, outputResult.code.toString());
logger.info(' created: "' + relativeOutfile + '"');
// write source map for the generated file
if (compileConfig.sourcemap !== false) {
var mapDir = path.join(compileConfig.dir.project, CONST.DIR.MAP);
outfile = path.join(mapDir, relativeOutfile) + '.' + CONST.FILE_EXT.MAP;
relativeOutfile = path.relative(compileConfig.dir.project, outfile);
fs.mkdirpSync(path.dirname(outfile));
fs.writeFileSync(outfile, JSON.stringify(sourceMap));
}
};
exports.generateSourceMap = function(generator, compileConfig) {
var target = generator.target;
var data = generator.data;
var origFile = generator.origFile;
var markers = _.map(data, function(v, k) { return k; });
var mapper = new SM.SourceMapGenerator({
file: path.join(compileConfig.dir.project, target.filename),
sourceRoot: compileConfig.dir.project
});
var genMap = {
count: 1,
code: ''
};
// passed in to mapLine so "sources" points at original src file
var theMap = {
count: 1, // used to track line numbers for source mapping
filename: origFile.filename // relative path to src file from project root
};
// initialize the rest of the generator properties
target.count = 1;
// gets text from original src file since template content is empty
// ensure target.templateContent will have a value so we can embed in sourcesContent if we want to
target.templateContent = getTextFromGenerator(null, origFile.filepath);
target.lines = target.templateContent.split(lineSplitter);
_.each(markers, function(m) {
var marker = data[m];
marker.count = 1;
// ensure marker.fileContent will have a value so we can embed in sourcesContent if we want to
marker.fileContent = getTextFromGenerator(marker.fileContent, marker.filename);
marker.lines = marker.fileContent.split(lineSplitter);
});
// generate the source map and composite code
// (should simply map every line from src -> dest)
_.each(target.lines, function(line) {
var trimmed = U.trim(line);
if (_.includes(markers, trimmed)) {
theMap.count++; // skip this line number in the input file now or else we'll be off by one from here on out
_.each(data[trimmed].lines, function(line) {
mapLine(mapper, data[trimmed], genMap, line);
});
} else {
mapLine(mapper, theMap, genMap, line);
}
});
// parse composite code into an AST
// TODO: Remove? This is a sanity check, I suppose, but is it necessary?
// Our classic build should blow up on bad JS files
var ast;
try {
ast = babylon.parse(genMap.code, {
sourceFilename: genMap.file,
sourceType: 'unambiguous',
allowReturnOutsideFunction: true,
});
} catch (e) {
const filename = path.relative(compileConfig.dir.project, generator.target.template);
U.dieWithCodeFrame(`Error parsing code in ${filename}. ${e.message}`, e.loc, genMap.code);
}
// TODO: We do not run the babel plugins (optimizer/builtins) here. Is that ok?
// TODO: embed sourcesContent into source map? Shouldn't need to since this is supposed to be a straight copy
// write source map for the generated file
var relativeOutfile = path.relative(compileConfig.dir.project, target.filepath);
var mapDir = path.join(compileConfig.dir.project, CONST.DIR.MAP);
var outfile = path.join(mapDir, relativeOutfile) + '.' + CONST.FILE_EXT.MAP;
fs.mkdirpSync(path.dirname(outfile));
fs.writeFileSync(outfile, JSON.stringify(mapper.toJSON()));
logger.debug(' map: "' + outfile + '"');
};