import { AstParser, ResolvablePromise } from '@uino/base-thing';
import { MathUtils } from '../math/MathUtils';
import { Utils } from '../common/Utils';
import { EventType } from '../const';
const __ = {
private: Symbol('private'),
}
const State = {
Ready: 0,
Loaded: 1,
Loading: 2,
}
const _varRegExp = /^([^\x00-\xff]|[a-zA-Z_$])([^\x00-\xff]|[a-zA-Z0-9_$])*$/;
const _defaultOptions = {};
// #region Private Functions
// Check variable name
function _checkVariableName(name) {
if (!_varRegExp.test(name)) {
return false;
}
return true;
}
// Parse static member variables.
function _parseStaticMemberVariables(code, classesNames) {
let index = 0;
let className = '';
// Replace 'static' keyword
while (true) {
let classIndex = code.substring(index).indexOf('class ');
if (classIndex !== -1) {
className = Utils.scanf(code.substring(classIndex), 'class %s ');
// Collect class name
classesNames.push(className);
}
// Try to find static member variable location
let startIndex = code.substring(index).indexOf('static ');
if (startIndex === -1) {
break;
}
startIndex += index;
// Get static member content
let content = '';
let endIndex = code.substring(startIndex).indexOf(';');
if (endIndex !== -1) {
content = code.substring(startIndex, startIndex + endIndex + 1);
// Keep going
index = startIndex + endIndex + 1;
}
else {
content = code.substring(startIndex);
// Keep going
index = startIndex + content.length;
}
// Get the static member variable name
let strings = Utils.scanf(content, 'static %s=%s');
let varName = strings[0]._trimBoth(' \t');
if (varName) {
// Check variable name
if (_checkVariableName(varName)) {
// Get variable value
let varValue = strings[1]._trimBoth(' \t;');
// Comment static variable declaration
code = code._removeAt(startIndex, content.length);
index -= content.length;
// Move the static variable here ... (just use the class.variable format)
code += `\n${className}_${_private.uuid}.${varName} = ${varValue};`;
}
}
}
return code;
}
// Parse code.
function _parseCode(code) {
let classNames = [];
code = _parseStaticMemberVariables(code, classNames);
return code;
}
// Load code.
function _loadCodeInES6Mode(sourceURL, code) {
return Utils.loadCodeAsync(code, {
es6Mode: true,
sourceURL
});
}
// #endregion
/**
* @class Bundle
* The bundle.
* @memberof THING
*/
class Bundle {
/**
* The resource bundle object what supports js script by loading with specified loader.
* @param {Object} param The initial parameters.
*/
constructor(param = {}) {
this[__.private] = {};
let _private = this[__.private];
_private.app = Utils.getCurrentApp();
// Becareful: uuid would be used to genereate code, so we need to remove all '-' from it
_private.uuid = MathUtils.generateUUID()._replaceAll('-', '');
_private.url = Utils.parseValue(param['url'], '');
_private.info = {};
_private.options = {};
_private.classesMappingTable = new Map();
_private.classNameInfo = {
main: '',
others: [],
};
_private.state = State.Ready;
_private.loadingPromise = new ResolvablePromise();
_private.reloadingPromise = null;
_private.callbacks = [];
_private.cache = new Map();
// Register bundle as global variables, in order to let script can get it
Utils.regsiterVariable(_private.uuid, this);
}
// #region Private
_resolveURL(relativeWoringPath, url) {
let _private = this[__.private];
if (url._startsWith('./')) {
return relativeWoringPath._appendPath(url);
}
else {
return _private.url._appendPath(url);
}
}
_linkStaticFunctions(className, options) {
let _private = this[__.private];
let code = `
THING.Utils.registerClass('${className}', ${className}, THING.Utils.getGlobal());
${className}.prototype.getBundle = function() {
return THING.Utils.getRegsiteredVariable('${_private.uuid}');
}
`;
let onLinkStaticFunctions = options['onLinkStaticFunctions'];
if (onLinkStaticFunctions) {
let externalCode = onLinkStaticFunctions(this, className);
if (externalCode) {
code += externalCode;
}
}
return code;
}
_buildClassName(node, options) {
let _private = this[__.private];
let className = node.id.name;
let latestClassName = className + `_${_private.uuid}`;
return latestClassName;
}
_getParentClassName(node) {
let loc = node.loc;
if (!loc) {
return '';
}
let startTokenIndex = node.loc.start.token;
if (!startTokenIndex) {
return '';
}
let tokens = node.loc.tokens;
let startToken = tokens[startTokenIndex];
// Search for 'extends' keyword to get the parent class name
for (let i = startTokenIndex; i >= 0; i--) {
let token = tokens[i];
let type = token.type;
let value = token.value;
if (type == 'Keyword' && value == 'extends') {
// Found it
return startToken.value;
}
else if (value == '{' || value == '}') {
// Miss-match
return '';
}
}
// Miss-match
return '';
}
_parseMainCode(ast, selfClassesMappingTable, requiredFiles, options) {
let _private = this[__.private];
// The current require file when using require() code
let curRequireFile = null;
// Get the main class name
let mainClassName = Utils.parseValue(_private.info.entrance, 'Main');
// Get require files and replace Main class name
let that = this;
AstParser.visit(ast, {
visitClassDeclaration: function (path) {
let { node } = path;
let className = node.id.name;
let latestClassName = that._buildClassName(node, options);
if (className == mainClassName) {
_private.classNameInfo.main = latestClassName;
}
node.id.name = latestClassName;
selfClassesMappingTable.set(className, latestClassName);
_private.classesMappingTable.set(className, latestClassName);
this.traverse(path);
},
visitIdentifier: function ({ node }) {
if (node.name == 'require') {
curRequireFile = {
filePath: '',
};
requiredFiles.push(curRequireFile);
// Make require as comment
node.name = '// require';
}
else {
// Try to get 'extends ParentClassName'
let className = that._getParentClassName(node);
if (className) {
let mappedClassName = _private.classesMappingTable.get(className);
if (mappedClassName) {
node.name = mappedClassName;
}
}
}
return false;
},
visitLiteral: function ({ node }) {
if (curRequireFile) {
curRequireFile.filePath = node.value;
curRequireFile = null;
}
return false;
},
});
}
_parseRequiredCode(ast, requiredClassesMappingTable, requiredFunctions, options) {
let that = this;
let _private = this[__.private];
AstParser.visit(ast, {
visitClassDeclaration: function (path) {
let { node } = path;
let className = node.id.name;
let latestClassName = that._buildClassName(node, options);
_private.classNameInfo.others.push(latestClassName);
requiredClassesMappingTable.set(className, latestClassName);
node.id.name = latestClassName;
this.traverse(path);
},
visitFunction: function (path) {
let { node } = path;
if (node.id) {
let latestFunctionName = node.id.name + `_${_private.uuid}`;
requiredFunctions.set(node.id.name, latestFunctionName);
node.id.name = latestFunctionName;
}
this.traverse(path);
},
visitIdentifier: function (path) {
let { node } = path;
let lastestFuncName = requiredFunctions.get(node.name);
if (lastestFuncName) {
node.name = lastestFuncName;
}
else {
let latestClassName = requiredClassesMappingTable.get(node.name);
if (latestClassName) {
node.name = latestClassName;
}
}
this.traverse(path);
}
});
}
_buildSourceURL(url, options) {
return url;
}
_compileAndLoadCode(url, code, options) {
// Get the relative working path for resources
let relativeWoringPath = url._getPath();
return new Promise((resolve, reject) => {
// The required files
let requiredFiles = [];
// The self classes mapping table
let selfClassesMappingTable = new Map();
// Parse the main code
let mainAst = AstParser.parse(_parseCode(code));
this._parseMainCode(mainAst, selfClassesMappingTable, requiredFiles, options);
// The required classes mapping table
let requiredClassesMappingTable = new Map();
// Load require files from bundle url
requiredFiles.waitForEachAsync(async file => {
let requiredFileUrl = this._resolveURL(relativeWoringPath, file.filePath);
let requiredFileCode = await Utils.loadTextFileAsync(requiredFileUrl);
let requiredFunctions = new Map();
// Parse required file code
let requiredFileAst = AstParser.parse(_parseCode(requiredFileCode));
this._parseRequiredCode(requiredFileAst, requiredClassesMappingTable, requiredFunctions, options);
// Get the final required file code
let finalRequiredFileCode = AstParser.print(requiredFileAst);
// Register required classes
if (requiredClassesMappingTable.size) {
finalRequiredFileCode += '\n';
requiredClassesMappingTable.forEach(className => {
finalRequiredFileCode += this._linkStaticFunctions(className, options);
});
}
let sourceURL = this._buildSourceURL(requiredFileUrl, options);
return _loadCodeInES6Mode(sourceURL, finalRequiredFileCode);
}).then(() => {
// Replace required class names
AstParser.visit(mainAst, {
visitIdentifier({ node }) {
let latestClassName = requiredClassesMappingTable.get(node.name);
if (latestClassName) {
node.name = latestClassName;
}
return false;
}
});
// Get the final main code
let finalMainCode = AstParser.print(mainAst);
// Register classes
if (selfClassesMappingTable.size) {
finalMainCode += '\n';
selfClassesMappingTable.forEach(className => {
finalMainCode += this._linkStaticFunctions(className, options);
});
}
let sourceURL = this._buildSourceURL(url, options);
_loadCodeInES6Mode(sourceURL, finalMainCode).then(
() => {
resolve();
},
(ev) => {
reject(ev);
},
);
});
});
}
_buildConfigCodeFromOptions(options) {
let _private = this[__.private];
let config = 'const bundleOptions = {\r\n';
config += `\tuuid: '${_private.uuid}',\r\n`;
config += '};\r\n\r\n';
return config;
}
_loadCode(url, code, options) {
let _private = this[__.private];
let sourceURL = this._buildSourceURL(url, options);
if (_private.cache.get(sourceURL)) {
return _private.cache.get(sourceURL)
}
let promise = new Promise((resolve, reject) => {
// Set some options as local variable to let script check in runtime
let config = this._buildConfigCodeFromOptions(options);
if (config) {
code = config + code;
}
// Load code in ES6 mode(it's compatible of es5 mode and below)
_loadCodeInES6Mode(sourceURL, code).then(
() => {
resolve();
},
(ev) => {
reject(ev);
},
);
});
_private.cache.set(sourceURL, promise);
return promise;
}
// #endregion
dispose() {
let _private = this[__.private];
let onDispose = _private.callbacks['onDispose'];
if (onDispose) {
onDispose.call(this);
}
_private.app = null;
_private.info = {};
_private.options = {};
_private.classesMappingTable.clear();
_private.callbacks = [];
_private.cache.clear();
_private.loadingPromise = null;
_private.reloadingPromise = null;
Utils.unregsiterVariable(_private.uuid, this);
}
/**
* Load code in bundle from data.
* @param {String} url The code url.
* @param {String} code The code string.
* @param {Object} options The options.
* @returns {Promise<any>}
* @private
*/
loadCodeFromData(url, code, options = _defaultOptions) {
let _private = this[__.private];
// Copy bundle options
_private.options = Object.assign({}, options);
// Compile and load script file in runtime
let autoCompile = Utils.parseValue(options['autoCompile'], false);
if (autoCompile) {
return this._compileAndLoadCode(url, code, options);
}
else {
return this._loadCode(url, code, options);
}
}
/**
* Load code in bundle from url.
* @param {String} url The resource url.
* @param {Object} options The options.
* @returns {Promise<any>}
* @private
*/
loadCodeFromUrl(url, options) {
return new Promise((resolve, reject) => {
Utils.loadTextFileAsync(url).then(data => {
this.loadCodeFromData(url, data, options).then(
() => {
resolve();
},
(ev) => {
reject(ev);
}
);
});
});
}
/**
* Reload bundle resource.
* @param {Array<Object>} loaders The bundle loaders.
* @param {Object} options The options.
* @returns {Promise<any>}
* @private
*/
reload(loaders, options = {}) {
let _private = this[__.private];
let type = _private.info.type;
let loader = loaders[type];
// Reload
if (loader && loader.onReload) {
_private.reloadingPromise = new Promise((resolve, reject) => {
loader.onReload(this, options,
() => {
let ev = { bundle: this };
_private.app.trigger(EventType.LoadBundle, ev);
resolve(ev);
},
(ev) => {
reject(ev);
}
);
});
return _private.reloadingPromise;
}
// Load in normal mode
else {
return this.load(loaders, options);
}
}
/**
* Load bundle resource.
* @param {Array<Object>} loaders The bundle loaders.
* @param {Object} options The options.
* @returns {Promise<any>}
* @private
*/
load(loaders, options = {}) {
let _private = this[__.private];
// If we had loaded then return pending promise(should be completed state)
if (_private.state == State.Loaded) {
return _private.loadingPromise;
}
// We are loading now
_private.state = State.Loading;
let bundleInfo = options['bundleInfo'];
if (bundleInfo) {
this._load(bundleInfo, loaders, options);
}
else {
// Get bundle file url
let bundleFileName = Utils.parseValue(options['bundleFileName'], 'bundle.json');
let bundleUrl = _private.url._appendPath(bundleFileName);
// Load main json file
Utils.loadJSONFile(bundleUrl,
// Load
(ev) => {
this._load(ev.data, loaders, options);
},
// Progress
() => { },
// Error
(ev) => {
Utils.error(`Load bundle '${bundleUrl}' resource failed, please make sure 'bundle.json' is existing`);
_private.loadingPromise.reject(ev);
}
);
}
return _private.loadingPromise;
}
_load(data, loaders, options) {
let _private = this[__.private];
_private.info = data;
let type = _private.info.type || 'scene';
let loader = loaders[type];
if (loader) {
let onCompolete = options['onComplete'] || options['complete'];
options['onComplete'] = options['complete'] = () => {
onCompolete && onCompolete(this);
_private.state = State.Loaded;
// Prevent the disposed bundle
if (_private.loadingPromise) {
_private.loadingPromise.resolve(this);
_private.app.trigger(EventType.LoadBundle, { bundle: this });
}
}
let onProgress = options['onProgress'] || options['progress'];
options['onProgress'] = options['progress'] = (progress) => {
onProgress && onProgress(progress);
}
let onError = options['onError'] || options['error'];
options['onError'] = options['error'] = (error) => {
onError && onError(error);
_private.loadingPromise.reject(error);
}
loader.onLoad(this, options);
}
else {
_private.loadingPromise.reject(`The bundle type '${type}' is unknown, should register bundler loader for it`);
}
}
waitForComplete() {
let _private = this[__.private];
if (_private.reloadingPromise) {
return _private.reloadingPromise;
}
return _private.loadingPromise;
}
/**
* Resolve URL from bundle root path.
* @param {String} url The url string.
* @returns {String}
* @private
*/
resolveURL(url) {
let _private = this[__.private];
return _private.url._appendURL(url);
}
/**
* Check whether had been loaded.
* @type {Boolean}
* @example
* if (bundle.loaded) {
* console.log('bundle has loaded');
* }
*/
get loaded() {
let _private = this[__.private];
return _private.state == State.Loaded;
}
/**
* Check whether it's loading.
* @type {Boolean}
* @example
* if (bundle.loading) {
* console.log('bundle is loading');
* }
*/
get loading() {
let _private = this[__.private];
return _private.state == State.Loading;
}
/**
* Get the unique ID.
* @type {String}
* @example
* console.log(bundle.uuid);
*/
get uuid() {
let _private = this[__.private];
return _private.uuid;
}
/**
* Get the resource url.
* @type {String}
* @example
* console.log(bundle.url);
*/
get url() {
let _private = this[__.private];
return _private.url;
}
/**
* Get the options.
* @type {Object}
* @example
* console.log(bundle.options);
*/
get options() {
let _private = this[__.private];
return _private.options;
}
/**
* Get the main info.
* @type {Object}
* @example
* let info = bundle.info;
* let name = bundle.name;
* console.log(`Bundle's name is '${name$}'`);
*/
get info() {
let _private = this[__.private];
return _private.info;
}
get classNameInfo() {
let _private = this[__.private];
return _private.classNameInfo;
}
get callbacks() {
let _private = this[__.private];
return _private.callbacks;
}
get isBundle() {
return true;
}
}
export { Bundle };