Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use SWC to parse and transform code instead of Babel #1365

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Initial poc on using swc to parse and transform code
ewanharris committed Oct 9, 2023
commit 26e6c7fe220dc9819b8b084a6a57b5805e667f87
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ var ERROR = 2;
module.exports = {
env: {
'node': true,
'es2017': true
'es2020': true
},

extends: 'eslint:recommended',
103 changes: 45 additions & 58 deletions Alloy/builtins/moment/lang/en-sg.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
//! moment.js locale configuration
//! locale : English (Singapore) [en-sg]
//! author : Matthew Castrillon-Madrigal : https://github.com/techdimension

;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
@@ -9,71 +7,60 @@
factory(global.moment)
}(this, (function (moment) { 'use strict';

//! moment.js locale configuration

var enSg = moment.defineLocale('en-sg', {
months: 'January_February_March_April_May_June_July_August_September_October_November_December'.split(
'_'
),
monthsShort: 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
weekdays: 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split(
'_'
),
weekdaysShort: 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
weekdaysMin: 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
longDateFormat: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'DD/MM/YYYY',
LL: 'D MMMM YYYY',
LLL: 'D MMMM YYYY HH:mm',
LLLL: 'dddd, D MMMM YYYY HH:mm',
var enSG = moment.defineLocale('en-SG', {
months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
longDateFormat : {
LT : 'HH:mm',
LTS : 'HH:mm:ss',
L : 'DD/MM/YYYY',
LL : 'D MMMM YYYY',
LLL : 'D MMMM YYYY HH:mm',
LLLL : 'dddd, D MMMM YYYY HH:mm'
},
calendar: {
sameDay: '[Today at] LT',
nextDay: '[Tomorrow at] LT',
nextWeek: 'dddd [at] LT',
lastDay: '[Yesterday at] LT',
lastWeek: '[Last] dddd [at] LT',
sameElse: 'L',
calendar : {
sameDay : '[Today at] LT',
nextDay : '[Tomorrow at] LT',
nextWeek : 'dddd [at] LT',
lastDay : '[Yesterday at] LT',
lastWeek : '[Last] dddd [at] LT',
sameElse : 'L'
},
relativeTime: {
future: 'in %s',
past: '%s ago',
s: 'a few seconds',
ss: '%d seconds',
m: 'a minute',
mm: '%d minutes',
h: 'an hour',
hh: '%d hours',
d: 'a day',
dd: '%d days',
M: 'a month',
MM: '%d months',
y: 'a year',
yy: '%d years',
relativeTime : {
future : 'in %s',
past : '%s ago',
s : 'a few seconds',
ss : '%d seconds',
m : 'a minute',
mm : '%d minutes',
h : 'an hour',
hh : '%d hours',
d : 'a day',
dd : '%d days',
M : 'a month',
MM : '%d months',
y : 'a year',
yy : '%d years'
},
dayOfMonthOrdinalParse: /\d{1,2}(st|nd|rd|th)/,
ordinal: function (number) {
ordinal : function (number) {
var b = number % 10,
output =
~~((number % 100) / 10) === 1
? 'th'
: b === 1
? 'st'
: b === 2
? 'nd'
: b === 3
? 'rd'
: 'th';
output = (~~(number % 100 / 10) === 1) ? 'th' :
(b === 1) ? 'st' :
(b === 2) ? 'nd' :
(b === 3) ? 'rd' : 'th';
return number + output;
},
week: {
dow: 1, // Monday is the first day of the week.
doy: 4, // The week that contains Jan 4th is the first week of the year.
},
week : {
dow : 1, // Monday is the first day of the week.
doy : 4 // The week that contains Jan 4th is the first week of the year.
}
});

return enSg;
return enSG;

})));
43 changes: 25 additions & 18 deletions Alloy/commands/compile/ast/builtins-plugin.js
Original file line number Diff line number Diff line change
@@ -2,12 +2,17 @@ var path = require('path'),
fs = require('fs'),
_ = require('lodash'),
logger = require('../../../logger'),
U = require('../../../utils');
U = require('../../../utils'),
{ Visitor } = require('@swc/core/Visitor');

var EXCLUDE = ['backbone', 'CFG', 'underscore'];
var BUILTINS_PATH = path.join(__dirname, '..', '..', '..', 'builtins');
var loaded = [];

function isRequire(n) {
return n.type === 'CallExpression' && n.callee.value == 'require';
}

function appendExtension(file, extension) {
extension = '.' + extension;
file = U.trim(file);
@@ -53,25 +58,26 @@ function loadMomentLanguages(config) {
}
}

module.exports = function (_ref) {
var types = _ref.types;
var rx = /^(\/?alloy)\/(.+)$/;
module.exports = class BuiltIns extends Visitor {
constructor(opts) {
super();
this.opts = opts;
}
visitCallExpression(n) {
const string = n.arguments[0];
if (
isRequire(n) &&
string.expression.type === 'StringLiteral' &&
string.expression.value.startsWith('/alloy')
) {
const match = string.expression.value.match(/^(\/?alloy)\/(.+)$/);

return {
visitor: {
CallExpression: function(p) {
var theString = p.node.arguments[0],
match;
if (p.node.callee.name === 'require' && // Is this a require call?
theString && types.isStringLiteral(theString) && // Is the 1st param a literal string?
(match = theString.value.match(rx)) !== null && // Is it an alloy module?
!_.includes(EXCLUDE, match[2]) && // Make sure it's not excluded.
!_.includes(loaded, match[2]) // Make sure we didn't find it already
) {
if (match) {
if (!EXCLUDE.includes(match[2]) && !loaded.includes(match[2])) {
// Make sure it hasn't already been copied to Resources
var name = appendExtension(match[2], 'js');
if (fs.existsSync(path.join(this.opts.dir.resources, match[1], name))) {
return;
return super.visitCallExpression(n);
}

// make sure the builtin exists
@@ -87,5 +93,6 @@ module.exports = function (_ref) {
}
}
}
};
};
return super.visitCallExpression(n);
}
};
120 changes: 43 additions & 77 deletions Alloy/commands/compile/ast/controller.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,62 @@
var U = require('../../../utils'),
babylon = require('@babel/parser'),
types = require('@babel/types'),
generate = require('@babel/generator').default,
{ default: traverse, Hub, NodePath } = require('@babel/traverse');

var isBaseControllerExportExpression = types.buildMatchMemberExpression('exports.baseController');

const GENCODE_OPTIONS = {
retainLines: true
};
swc = require('@swc/core'),
{ Visitor } = require('@swc/core/Visitor');

exports.processController = function(code, file) {
var baseController = '',
moduleCodes = '',
newCode = '',
exportSpecifiers = [];
exportSpecifiers = [],
es6mods;

try {
var ast = babylon.parse(code, { sourceFilename: file, sourceType: 'unambiguous' });
const x = swc.parseSync(code);
const plugin = new ProcessController();
plugin.visitModule(x);
newCode = swc.printSync(x).code;
es6mods = plugin.moduleCodes;
} catch (e) {
U.dieWithCodeFrame('Error generating AST for "' + file + '". Unexpected token at line ' + e.loc.line + ' column ' + e.loc.column, e.loc, code);
}

const hub = new Hub();
hub.buildError = function (node, message, Error) {
const loc = node && node.loc;
const err = new Error(message);
return {
es6mods: es6mods,
base: baseController,
code: newCode
};
};

if (loc) {
err.loc = loc.start;
}
class ProcessController extends Visitor {
constructor() {
super();

return err;
};
const path = NodePath.get({
hub: hub,
parent: ast,
container: ast,
key: 'program'
}).setContext();
traverse(ast, {
enter: function(path) {
if (types.isAssignmentExpression(path.node) && isBaseControllerExportExpression(path.node.left)) {
// what's equivalent of print_to_string()? I replaced with simple value property assuming it's a string literal
baseController = '\'' + path.node.right.value + '\'';
}
},
this.moduleCodes = '';

ImportDeclaration: function(path) {
moduleCodes += generate(path.node, GENCODE_OPTIONS).code;
path.remove();
},
}

ExportNamedDeclaration: function(path) {
var node = path.node;
var specifiers = node.specifiers;
if (specifiers && specifiers.length !== 0) {
specifiers.forEach(function (specifier) {
if (specifier.local && specifier.local.name) {
exportSpecifiers.push(specifier.local.name);
}
});
}
moduleCodes += generate(node, GENCODE_OPTIONS).code;
path.remove();
}
}, path.scope);
visitAssignmentExpression(node) {
if (node.left.property.value === 'baseController') {
baseController = node.right.raw;
}
return super.visitAssignmentExpression(node);
}

if (exportSpecifiers.length > 0) {
traverse(ast, {
enter: function(path) {
var node = path.node,
name;
if (node.type === 'VariableDeclaration') {
name = node.declarations[0].id.name;
} else if (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') {
name = node.id.name;
}
visitModuleItems(nodes) {
const transformed = [];
for (const node of nodes) {

if (exportSpecifiers.indexOf(name) !== -1) {
moduleCodes += generate(node, GENCODE_OPTIONS).code;
path.remove();
}
}
if (node.type !== 'ImportDeclaration' && node.type !== 'ExportDeclaration') {
transformed.push(node);
continue;
}
const mod = swc.printSync({
type:'Module',
body: [node],
span: node.span
});
this.moduleCodes += mod.code;
}

newCode = generate(ast, GENCODE_OPTIONS).code;
} catch (e) {
U.dieWithCodeFrame('Error generating AST for "' + file + '". Unexpected token at line ' + e.loc.line + ' column ' + e.loc.column, e.loc, code);
return transformed;
}

return {
es6mods: moduleCodes,
base: baseController,
code: newCode
};
};
}
171 changes: 109 additions & 62 deletions Alloy/commands/compile/ast/optimizer-plugin.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,118 @@
var CONST = require('../../../common/constants'),
_ = require('lodash'),
path = require('path'),
fs = require('fs');
fs = require('fs'),
{ Visitor } = require('@swc/core/Visitor');

// Walk tree transformer changing (Ti|Titanium).Platform.(osname|name)
// into static strings where possible. This will allow the following
// compression step to reduce the code further.
module.exports = function (_ref) {
var types = _ref.types;

var isTiPlatform = types.buildMatchMemberExpression('Ti.Platform');
var isTitaniumPlatform = types.buildMatchMemberExpression('Titanium.Platform');

return {
pre: function(state) {
var config = this.opts || {};
config.deploytype = config.deploytype || 'development';

// create list of platform and deploy type defines
var defines = {};
_.each(CONST.DEPLOY_TYPES, function(d) {
defines[d.key] = config.deploytype === d.value;
});
_.each(CONST.DIST_TYPES, function(d) {
defines[d.key] = _.includes(d.value, config.target);
});
_.each(CONST.PLATFORMS, function(p) {
defines['OS_' + p.toUpperCase()] = config.platform === p;
});
this.defines = defines;

// make sure the platform require includes
var platformString = config.platform.toLowerCase();
var platformPath = path.join(__dirname, '..', '..', '..', '..', 'platforms', platformString, 'index');
if (!fs.existsSync(platformPath + '.js')) {
this.platform = {name: undefined, osname: undefined };
} else {
// create, transform, and validate the platform object
this.platform = require(platformPath);
if (!_.isString(this.platform.name)) { this.platform.name = undefined; }
if (!_.isString(this.platform.osname)) { this.platform.osname = undefined; }
}
},
visitor: {
MemberExpression: function(path, state) {
// console.log(JSON.stringify(path.node));
var name = '';
if (types.isStringLiteral(path.node.property)) {
name = path.node.property.value;
} else if (types.isIdentifier(path.node.property)) {
name = path.node.property.name;
} else {
return;
}

if ((name === 'name' || name === 'osname') && this.platform[name]) {
if (isTiPlatform(path.node.object) || isTitaniumPlatform(path.node.object)) {
path.replaceWith(types.stringLiteral(this.platform[name]));
}
}
},
Identifier: function(path) {
if (Object.prototype.hasOwnProperty.call(this.defines, path.node.name) &&
(path.parent.type !== 'VariableDeclarator' || path.node.name !== path.parent.id.name)) {
path.replaceWith(types.booleanLiteral(this.defines[path.node.name]));
}
function isTiPlatform(member, parts) {
if (member.type !== 'MemberExpression') {
return false;
}

const nodes = [];
let n;
for (n = member; n.type === 'MemberExpression'; n = n.object) {
nodes.push(n.property);
}
nodes.push(member.object);

if (nodes.length !== 2) {
return false;
}

for (let i = 0, j = nodes.length - 1; i < parts.length; i++, j--) {
const node = nodes[j];
let value;
if (node.type === 'Identifier' || node.type === 'StringLiteral') {
value = node.value;
} else if (node.type === 'ThisExpression') {
value = 'this';
} else {
return false;
}

if (parts[i] !== value) {
return false;
}
}
return true;
}

module.exports = class Optimizer extends Visitor {
constructor(alloyConfig) {
super();
this.defines = {};
this.dirty = false;

alloyConfig.deploytype = alloyConfig.deploytype || 'development';


for (const deployType of CONST.DEPLOY_TYPES) {
this.defines[deployType.key] = alloyConfig.deployType === deployType.value;
}

for (const distType of CONST.DIST_TYPES) {
this.defines[distType.key] = distType.value.includes(alloyConfig.target);
}

for (const platform of CONST.PLATFORMS) {
this.defines[`OS_${platform.toUpperCase()}`] = alloyConfig.platform === platform;
}

var platformString = alloyConfig.platform.toLowerCase();
var platformPath = path.join(__dirname, '..', '..', '..', '..', 'platforms', platformString, 'index');
if (!fs.existsSync(platformPath + '.js')) {
this.platform = {name: undefined, osname: undefined };
} else {
// create, transform, and validate the platform object
this.platform = require(platformPath);
if (!_.isString(this.platform.name)) { this.platform.name = undefined; }
if (!_.isString(this.platform.osname)) { this.platform.osname = undefined; }
}

}

visitMemberExpression(node) {
let name;
if (node.property.type === 'StringLiteral' || node.property.type === 'Identifier') {
name = node.property.value;
} else if (node.property.type === 'Computed') {
name = node.property.expression.value;
} else {
return;
}

if ((name === 'name' || name === 'osname') && this.platform[name]) {
if (isTiPlatform(node.object, ['Ti', 'Platform']) || isTiPlatform(node.object, ['Titanium', 'Platform'])) {
this.dirty = true;
return {
...node,
type: 'StringLiteral',
span: node.span,
value: this.platform[name]
};
}
}
};
};

return super.visitMemberExpression(node);
}

visitIdentifier(node) {
const name = node.value;

if (Object.hasOwn(this.defines, name)) {
this.dirty = true;
return {
...node,
type: 'BooleanLiteral',
span: node.span,
value: this.defines[name]
};
}

return super.visitIdentifier(node);
}
};
26 changes: 15 additions & 11 deletions Alloy/commands/compile/index.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@ var ejs = require('ejs'),
fs = require('fs-extra'),
walkSync = require('walk-sync'),
vm = require('vm'),
babel = require('@babel/core'),
async = require('async'),

// alloy requires
@@ -20,7 +19,10 @@ var ejs = require('ejs'),
sourceMapper = require('./sourceMapper'),
CompilerMakeFile = require('./CompilerMakeFile'),
BuildLog = require('./BuildLog'),
Orphanage = require('./Orphanage');
Orphanage = require('./Orphanage'),
swc = require('@swc/core'),
BuiltIns = require('./ast/builtins-plugin'),
Optimizer = require('./ast/optimizer-plugin');

var alloyRoot = path.join(__dirname, '..', '..'),
viewRegex = new RegExp('\\.' + CONST.FILE_EXT.VIEW + '$'),
@@ -1155,17 +1157,19 @@ function optimizeCompiledCode(alloyConfig, paths) {

while ((files = _.difference(getJsFiles(), lastFiles)).length > 0) {
_.each(files, function(file) {
var options = _.extend(_.clone(sourceMapper.OPTIONS_OUTPUT), {
plugins: [
[require('./ast/builtins-plugin'), compileConfig],
[require('./ast/optimizer-plugin'), compileConfig.alloyConfig],
]
}),
fullpath = path.join(compileConfig.dir.resources, file);
const fullpath = path.join(compileConfig.dir.resources, file);

logger.info('- ' + file);
try {
var result = babel.transformFileSync(fullpath, options);

const x = swc.parseFileSync(fullpath);
const plugin = new BuiltIns(compileConfig);
plugin.visitModule(x);
const optimizer = new Optimizer(compileConfig.alloyConfig);
optimizer.visitModule(x);

const result = swc.printSync(x);

fs.writeFileSync(fullpath, result.code);
} catch (e) {
U.die('Error transforming JS file', e);
@@ -1200,4 +1204,4 @@ function BENCHMARK(desc, isFinished) {
logger.info('');
logger.info('Alloy compiled in ' + thisTime + 's');
}
}
}
127 changes: 64 additions & 63 deletions Alloy/commands/compile/sourceMapper.js
Original file line number Diff line number Diff line change
@@ -10,7 +10,10 @@ var SM = require('source-map'),
babylon = require('@babel/parser'),
babel = require('@babel/core'),
logger = require('../../logger'),
_ = require('lodash');
_ = require('lodash'),
swc = require('@swc/core'),
builtins = require('./ast/builtins-plugin'),
Optimizer = require('./ast/optimizer-plugin');

var lineSplitter = /(?:\r\n|\r|\n)/;

@@ -26,18 +29,7 @@ exports.OPTIONS_OUTPUT = {
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
});
function mapLine(genMap, line) {
genMap.code += line + '\n';
}

@@ -59,10 +51,10 @@ exports.generateCodeAndSourceMap = function(generator, compileConfig) {
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
});
// 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) {
@@ -104,51 +96,60 @@ exports.generateCodeAndSourceMap = function(generator, compileConfig) {
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);
mapLine(genMap, line);
});
} else {
mapLine(mapper, templateMap, genMap, line);
mapLine(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);
}
// 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);
// 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);

const x = swc.parseSync(genMap.code);
const plugin = new builtins(compileConfig);
plugin.visitModule(x);

const optimizer = new Optimizer(compileConfig.alloyConfig);
optimizer.visitModule(x);

const outputResult = swc.printSync(x, {filename: filename, sourceMaps: true });

// produce the source map and embed the original source (so the template source can be passed along)
const sourceMap = mapper.toJSON();
const sourceMap = JSON.parse(outputResult.map);
sourceMap.sourcesContent = [ target.templateContent, data[markers[0]].fileContent ];

sourceMap.sources = [templateMap.filename, filename];
// 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}`;

@@ -217,18 +218,18 @@ exports.generateSourceMap = function(generator, compileConfig) {
// 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);
}
// 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
287 changes: 287 additions & 0 deletions package-lock.json
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -29,14 +29,15 @@
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.18.15",
"@babel/types": "^7.20.0",
"@prantlf/jsonlint": "11.7.0",
"@swc/core": "^1.3.92",
"@xmldom/xmldom": "^0.8.5",
"async": "^3.2.4",
"colors": "1.4.0",
"commander": "^9.4.1",
"ejs": "3.1.8",
"fs-extra": "^10.1.0",
"global-paths": "^1.0.0",
"@prantlf/jsonlint": "11.7.0",
"lodash": "^4.17.4",
"moment": "2.29.4",
"node.extend": "2.0.2",
97 changes: 56 additions & 41 deletions test/specs/optimizer.js
Original file line number Diff line number Diff line change
@@ -6,33 +6,35 @@ var fs = require('fs'),
_ = require('lodash'),
sourceMapper = require('../../Alloy/commands/compile/sourceMapper'),
babylon = require('@babel/parser'),
babel = require('@babel/core');
babel = require('@babel/core'),
swc = require('@swc/core'),
Optimizer = require('../../Alloy/commands/compile/ast/optimizer-plugin');

var tests = [
// make sure we didn't break normal conditionals and assigments
['var test = {\n a: 0,\n b:0,\n c: 0};\ntest.b = 1', 'var test = {\n a: 0,\n b: 0,\n c: 0 };\ntest.b = 1;'],
['var test = {\n a: 0,\n b:0,\n c: 0\n};\ntest.b = 1', 'var test = {\n a: 0,\n b: 0,\n c: 0\n};\ntest.b = 1;'],
['var a = Ti.Platform.name', 'var a = "<%= name %>";'],
['var a = Titanium.Platform.name', 'var a = "<%= name %>";'],
['var a = Ti.Platform.name=="<%= name %>" ? 1 : 0', 'var a = "<%= name %>" == "<%= name %>" ? 1 : 0;'],
['var a = Ti.Platform.name=="<%= name %>",\nb', 'var a = "<%= name %>" == "<%= name %>",\n b;'],
['var a = Ti.Platform.name=="<%= name %>",\nb,\nc = 2', 'var a = "<%= name %>" == "<%= name %>",\n b,\n c = 2;'],
['var a = Ti.Platform.name=="<%= name %>", b', 'var a = "<%= name %>" == "<%= name %>", b;'],
['var a = Ti.Platform.name=="<%= name %>", b, c = 2', 'var a = "<%= name %>" == "<%= name %>", b, c = 2;'],
['var a = Ti.Platform.name=="<%= name %>"', 'var a = "<%= name %>" == "<%= name %>";'],
['var a,\nb = Ti.Platform.name=="<%= name %>",\nc = 2;', 'var a,\n b = "<%= name %>" == "<%= name %>",\n c = 2;'],
['var a, b = Ti.Platform.name=="<%= name %>", c = 2;', 'var a, b = "<%= name %>" == "<%= name %>", c = 2;'],
['var a = "<%= name %>"==Ti.Platform.name ? 1 : 0', 'var a = "<%= name %>" == "<%= name %>" ? 1 : 0;'],
['var a = "<%= name %>"==Ti.Platform.name,\nb', 'var a = "<%= name %>" == "<%= name %>",\n b;'],
['var a = "<%= name %>"==Ti.Platform.name,\nb,\nc = 2', 'var a = "<%= name %>" == "<%= name %>",\n b,\n c = 2;'],
['var a = "<%= name %>"==Ti.Platform.name, b', 'var a = "<%= name %>" == "<%= name %>", b;'],
['var a = "<%= name %>"==Ti.Platform.name, b, c = 2', 'var a = "<%= name %>" == "<%= name %>", b, c = 2;'],
['var a = "<%= name %>"==Ti.Platform.name', 'var a = "<%= name %>" == "<%= name %>";'],
['var a,\nb = "<%= name %>"==Ti.Platform.name,\nc = 2;', 'var a,\n b = "<%= name %>" == "<%= name %>",\n c = 2;'],
['var a, b = "<%= name %>"==Ti.Platform.name, c = 2;', 'var a, b = "<%= name %>" == "<%= name %>", c = 2;'],
['var a = "1"', 'var a = "1";'],
['var a = true', 'var a = true;'],
['var a = 1', 'var a = 1;'],
['var a', 'var a;'],
['var a = {}', 'var a = {};'],
['var a = new Object', 'var a = new Object();'],
['var a = new Object;', 'var a = new Object;'],
['var a = new Object()', 'var a = new Object();'],
['var a = Ti.Platform.name', 'var a = "<%= name %>";'],
['var a = Ti.Platform.osname', 'var a = "android";', ['android']],
['var a,\nb = 1,\nc = 2;', 'var a,\n b = 1,\n c = 2;'],
['var a, b = 1, c = 2;', 'var a, b = 1, c = 2;'],
['var a = 1;', 'var a = 1;'],
['var a =+1;', 'var a = +1;'],
['var a =1+1;', 'var a = 1 + 1;'],
@@ -41,43 +43,43 @@ var tests = [
['var a = -1.02;', 'var a = -1.02;'],
['var a = false', 'var a = false;'],
['var a = true ? 1 : 0;', 'var a = true ? 1 : 0;'],
["var num = isNaN(amount) || amount === '' || amount === null ? 0.00 : amount;", 'var num = isNaN(amount) || amount === \'\' || amount === null ? 0.00 : amount;'],
['var num = isNaN(amount) || amount === "" || amount === null ? 0.00 : amount;', 'var num = isNaN(amount) || amount === "" || amount === null ? 0.00 : amount;'],

// TODO: Revisit all "var a,a=2;" expecteds once ALOY-540 is resolved
// // TODO: Revisit all "var a,a=2;" expecteds once ALOY-540 is resolved

// make sure we didn't break normal if conditions
// // make sure we didn't break normal if conditions
['if (true) {\n var a = 1;\n} else {\n var a = 2;\n}', "if (true) {\n var a = 1;\n} else {\n var a = 2;\n}"],

// check platform conditionals (if/else)
["if (Titanium.Platform.name === '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", "if (\"<%= name %>\" === '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}"],
["if (Titanium.Platform.name !== '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", "if (\"<%= name %>\" !== '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}"],
["if (Titanium.Platform['name'] == '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", "if (\"<%= name %>\" == '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}"],
["if (Titanium.Platform.name !== '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", "if (\"<%= name %>\" !== '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}"],
["if (Titanium.Platform['name'] !== '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", "if (\"<%= name %>\" !== '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}"],
// // check platform conditionals (if/else)
["if (Titanium.Platform.name === '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", 'if ("<%= name %>" === "<%= name %>") {\n var a = 1;\n} else {\n var a = 2;\n}'],
["if (Titanium.Platform.name !== '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", 'if ("<%= name %>" !== "<%= name %>") {\n var a = 1;\n} else {\n var a = 2;\n}'],
["if (Titanium.Platform['name'] == '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", 'if (\"<%= name %>\" == "<%= name %>") {\n var a = 1;\n} else {\n var a = 2;\n}'],
["if (Titanium.Platform.name !== '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", 'if (\"<%= name %>\" !== "<%= name %>") {\n var a = 1;\n} else {\n var a = 2;\n}'],
["if (Titanium.Platform['name'] !== '<%= name %>') {\n var a = 1;\n} else {\n var a = 2;\n}", 'if (\"<%= name %>\" !== "<%= name %>") {\n var a = 1;\n} else {\n var a = 2;\n}'],

// check platform conditional assignments
["var platform = Ti.Platform['name'] === '<%= name %>'", "var platform = \"<%= name %>\" === '<%= name %>';"],
["var platform = Ti.Platform[\"name\"] === '<%= name %>'", "var platform = \"<%= name %>\" === '<%= name %>';"],
["var platform = Ti.Platform.name === '<%= name %>'", "var platform = \"<%= name %>\" === '<%= name %>';"],
["var platform = (Ti.Platform.name === '<%= name %>') ? 1 : 0", "var platform = \"<%= name %>\" === '<%= name %>' ? 1 : 0;"],
["var platform = (Ti.Platform.name === '<%= name %>') ? true : false", "var platform = \"<%= name %>\" === '<%= name %>' ? true : false;"],
["var platform = Ti.Platform['name'] === '<%= name %>'", "var platform = \"<%= name %>\" === \"<%= name %>\";"],
["var platform = Ti.Platform[\"name\"] === '<%= name %>'", "var platform = \"<%= name %>\" === \"<%= name %>\";"],
["var platform = Ti.Platform.name === '<%= name %>'", "var platform = \"<%= name %>\" === \"<%= name %>\";"],
["var platform = (Ti.Platform.name === '<%= name %>') ? 1 : 0", "var platform = (\"<%= name %>\" === \"<%= name %>\") ? 1 : 0;"],
["var platform = (Ti.Platform.name === '<%= name %>') ? true : false", "var platform = (\"<%= name %>\" === \"<%= name %>\") ? true : false;"],

// check identities
// // check identities
["var a = Ti.Platform.name === Titanium.Platform.name","var a = \"<%= name %>\" === \"<%= name %>\";"],

// shouldn't attempt to process anything other than strings
// // shouldn't attempt to process anything other than strings
["if (Ti.Platform.name === couldBeAnything()) {\n var a = 1;\n} else {\n var a = 2;\n}","if (\"<%= name %>\" === couldBeAnything()) {\n var a = 1;\n} else {\n var a = 2;\n}"],
["if (Ti.Platform.name === some.Other.Value) {\n var a = 1;\n} else {\n var a = 2;\n}","if (\"<%= name %>\" === some.Other.Value) {\n var a = 1;\n} else {\n var a = 2;\n}"],
["if (Ti.Platform.name !== aVariable) {\n var a = 1;\n} else {\n var a = 2;\n}","if (\"<%= name %>\" !== aVariable) {\n var a = 1;\n} else {\n var a = 2;\n}"],

// properly handles conditionals without curly braces
["if (Ti.Platform.name === '<%= name %>') var a = 1; else var a = 2;", "if (\"<%= name %>\" === '<%= name %>') var a = 1;else var a = 2;"],
["if (Ti.Platform.name !== '<%= name %>') var a = 1; else var a = 2;", "if (\"<%= name %>\" !== '<%= name %>') var a = 1;else var a = 2;"],
["if ('<%= name %>' === Ti.Platform.name) var a = 1; else var a = 2;", "if ('<%= name %>' === \"<%= name %>\") var a = 1;else var a = 2;"],
["if ('<%= name %>' !== Ti.Platform.name) var a = 1; else var a = 2;", "if ('<%= name %>' !== \"<%= name %>\") var a = 1;else var a = 2;"],
["if (Ti.Platform.name === '<%= name %>') var a = 1; else var a = 2;", "if (\"<%= name %>\" === \"<%= name %>\") var a = 1;\nelse var a = 2;"],
["if (Ti.Platform.name !== '<%= name %>') var a = 1; else var a = 2;", "if (\"<%= name %>\" !== \"<%= name %>\") var a = 1;\nelse var a = 2;"],
["if ('<%= name %>' === Ti.Platform.name) var a = 1; else var a = 2;", "if (\"<%= name %>\" === \"<%= name %>\") var a = 1;\nelse var a = 2;"],
["if ('<%= name %>' !== Ti.Platform.name) var a = 1; else var a = 2;", "if (\"<%= name %>\" !== \"<%= name %>\") var a = 1;\nelse var a = 2;"],

// works if Ti.Platform.* is on the left or right hand side
["if ('<%= name %>' === Ti.Platform.name) {\n var a = 1;\n} else {\n a = 2;\n}", "if ('<%= name %>' === \"<%= name %>\") {\n var a = 1;\n} else {\n a = 2;\n}"],
// // works if Ti.Platform.* is on the left or right hand side
["if ('<%= name %>' === Ti.Platform.name) {\n var a = 1;\n} else {\n a = 2;\n}", "if (\"<%= name %>\" === \"<%= name %>\") {\n var a = 1;\n} else {\n a = 2;\n}"],

['var a = OS_IOS', 'var a = true;', ['ios']],
['var a = OS_ANDROID', 'var a = true;', ['android']]
@@ -116,19 +118,32 @@ describe('optimizer.js', function() {
// execute the squeeze to remove dead code, always performed
// as the last step of JS file processing. The unit testing here
// uses the same settings as the Alloy compile process.
var squeezeFunction = function() {
var options = _.extend(_.clone(sourceMapper.OPTIONS_OUTPUT), {
plugins: [['./Alloy/commands/compile/ast/optimizer-plugin', {platform: platform}]]
});
var result = babel.transformFromAstSync(ast, null, options);
ast = result.ast;
code = result.code.replace(/\s*$/,'');
};
expect(squeezeFunction).not.toThrow();
// var squeezeFunction = function() {
// var options = _.extend(_.clone(sourceMapper.OPTIONS_OUTPUT), {
// plugins: [['./Alloy/commands/compile/ast/optimizer-plugin', {platform: platform}]]
// });
// var result = babel.transformFromAstSync(ast, null, options);
// ast = result.ast;
// code = result.code.replace(/\s*$/,'');
// };
// expect(squeezeFunction).not.toThrow();

function optimize() {
const x = swc.parseSync(testContent);

const optimizer = new Optimizer({platform: platform});
optimizer.visitModule(x);
const result = swc.printSync(x)
code = result.code.replace(/\s{4}/g, '\n ').replace(/\s*$/g,'');
}

expect(optimize).not.toThrow();
});
it(prefix + 'generated code matches expected code', function() {
var passFor = test[2];
var expected = _.template(test[1])(platforms[platform]);
console.log(code);
console.log(expected);
if (!passFor || _.includes(passFor, platform)) {
expect(code).toBe(expected);
} else {