@qooxdoo/framework
Version:
The JS Framework for Coders
520 lines (461 loc) • 15.8 kB
JavaScript
/* ************************************************************************
*
* qooxdoo-compiler - node.js based replacement for the Qooxdoo python
* toolchain
*
* https://github.com/qooxdoo/qooxdoo
*
* Copyright:
* 2011-2017 Zenesis Limited, http://www.zenesis.com
*
* License:
* MIT: https://opensource.org/licenses/MIT
*
* This software is provided under the same licensing terms as Qooxdoo,
* please see the LICENSE file in the Qooxdoo project's top-level directory
* for details.
*
* Authors:
* * John Spackman (john.spackman@zenesis.com, @johnspackman)
*
* *********************************************************************** */
/* eslint-disable @qooxdoo/qx/no-illegal-private-usage */
var path = require("upath");
var log = qx.tool.utils.LogManager.createLog("resource-manager");
/**
* Analyses library resources, collecting information into a cached database
* file
*/
qx.Class.define("qx.tool.compiler.resources.Manager", {
extend: qx.core.Object,
/**
* Constructor
*
* @param analyser {qx.tool.compiler.Analyser}
*/
construct(analyser) {
super();
this.__analyser = analyser;
this.__dbFilename = analyser.getResDbFilename() || "resource-db.json";
this.__loaders = [
new qx.tool.compiler.resources.ImageLoader(this),
new qx.tool.compiler.resources.MetaLoader(this)
];
this.__converters = [
new qx.tool.compiler.resources.ScssConverter(),
new qx.tool.compiler.resources.ScssIncludeConverter()
];
},
members: {
/** {String} filename of database */
__dbFilename: null,
/** {Object} Database */
__db: null,
/** the used analyser */
__analyser: null,
/** {Map{String,Library}} Lookup of libraries, indexed by resource URI */
__librariesByResourceUri: null,
/** {String[]} Array of all resource URIs, sorted alphabetically (ie these are the keys in __librariesByResourceUri) */
__allResourceUris: null,
/** {ResourceLoader[]} list of resource loaders, used to add info to the database */
__loaders: null,
/** {ResourceConverter[]} list of resource converters, used to copy resources to the target */
__converters: null,
/**
* Loads the cached database
*/
async loadDatabase() {
this.__db =
(await qx.tool.utils.Json.loadJsonAsync(this.__dbFilename)) || {};
},
/**
* Saves the database
*/
async saveDatabase() {
log.debug("saving resource manager database");
return qx.tool.utils.Json.saveJsonAsync(this.__dbFilename, this.__db);
},
/**
* Returns the loaded database
*
* @returns
*/
getDatabase() {
return this.__db;
},
/**
* Finds the library needed for a resource, see `findLibrariesForResource`. This reports
* an error if more than one library is found.
*
* @param uri {String} URI
* @return {qx.tool.compiler.app.Library[]} the libraries, empty list if not found
*/
findLibraryForResource(uri) {
let result = this.findLibrariesForResource(uri);
if (result.length == 0) {
return null;
}
if (result.length > 1) {
qx.tool.compiler.Console.error(
`Cannot determine a single library for the URI '${uri}'; ` +
`found ${result
.map(l => l.getNamespace())
.join(",")} returning first library`
);
}
return result[0];
},
/**
* Finds the libraries needed for a resource; this depends on `findAllResources` having
* already been called. `uri` can include optional explicit namespace (eg "qx:blah/blah.png"),
* otherwise the library resource lookups are examined to find the library.
*
* Note that there can be more than one directory because the lookup holds directory names (used
* for wildcards) and they are allowed to be duplicated.
*
* @param uri {String} URI
* @return {qx.tool.compiler.app.Library[]} the libraries, empty list if not found
*/
findLibrariesForResource(uri) {
const findLibrariesForResourceImpl = () => {
var ns;
var pos;
// check for absolute path first, in windows c:/ is a valid absolute name
if (path.isAbsolute(uri)) {
let library = this.__analyser
.getLibraries()
.find(lib => uri.startsWith(path.resolve(lib.getRootDir())));
return library || null;
}
// Explicit library?
pos = uri.indexOf(":");
if (pos !== -1) {
ns = uri.substring(0, pos);
let library = this.__analyser.findLibrary(ns);
return library || null;
}
// Non-wildcards are a direct lookup
// check for $ and *. less pos wins
// fix for https://github.com/qooxdoo/qooxdoo/issues/260
var pos1 = uri.indexOf("$"); // Variable references are effectively a wildcard lookup
var pos2 = uri.indexOf("*");
if (pos1 === -1) {
pos = pos2;
} else if (pos2 === -1) {
pos = pos1;
} else {
pos = Math.min(pos1, pos2);
}
if (pos === -1) {
let library = this.__librariesByResourceUri[uri] || null;
return library;
}
// Strip wildcard
var isFolderMatch = uri[pos - 1] === "/";
uri = uri.substring(0, pos - 1);
// Fast folder match
if (isFolderMatch) {
let library = this.__librariesByResourceUri[uri] || null;
return library;
}
// Slow scan
if (!this.__allResourceUris) {
this.__allResourceUris = Object.keys(
this.__librariesByResourceUri
).sort();
}
var thisUriPos = qx.tool.utils.Values.binaryStartsWith(
this.__allResourceUris,
uri
);
if (thisUriPos > -1) {
let libraries = {};
for (; thisUriPos < this.__allResourceUris.length; thisUriPos++) {
var thisUri = this.__allResourceUris[thisUriPos];
if (!thisUri.startsWith(uri)) {
break;
}
pos = uri.indexOf(":");
if (pos !== -1) {
ns = uri.substring(0, pos);
if (!libraries[ns]) {
libraries[ns] = this.__analyser.findLibrary(ns);
}
}
}
return Object.values(libraries);
}
return null;
};
let result = findLibrariesForResourceImpl();
if (!result) {
return [];
}
if (!qx.lang.Type.isArray(result)) {
return [result];
}
return result;
},
/**
* Scans all libraries looking for resources; this does not analyse the
* files, simply compiles the list
*/
async findAllResources() {
var t = this;
var db = this.__db;
if (!db.resources) {
db.resources = {};
}
t.__librariesByResourceUri = {};
this.__allResourceUris = null;
this.__assets = {};
await qx.Promise.all(
t.__analyser.getLibraries().map(async library => {
var resources = db.resources[library.getNamespace()];
if (!resources) {
db.resources[library.getNamespace()] = resources = {};
}
var unconfirmed = {};
for (let relFile in resources) {
unconfirmed[relFile] = true;
}
const scanResources = async resourcePath => {
// If the root folder exists, scan it
var rootDir = path.join(
library.getRootDir(),
library.get(resourcePath)
);
await qx.tool.utils.files.Utils.findAllFiles(
rootDir,
async filename => {
var relFile = filename
.substring(rootDir.length + 1)
.replace(/\\/g, "/");
var fileInfo = resources[relFile];
delete unconfirmed[relFile];
if (!fileInfo) {
fileInfo = resources[relFile] = {};
}
fileInfo.resourcePath = resourcePath;
fileInfo.mtime = await qx.tool.utils.files.Utils.safeStat(
filename
).mtime;
let asset = new qx.tool.compiler.resources.Asset(
library,
relFile,
fileInfo
);
this.__addAsset(asset);
}
);
};
await scanResources("resourcePath");
await scanResources("themePath");
// Check the unconfirmed resources to make sure that they still exist;
// delete from the database if they don't
await qx.Promise.all(
Object.keys(unconfirmed).map(async filename => {
let fileInfo = resources[filename];
if (!fileInfo) {
delete resources[filename];
} else {
let stat = await qx.tool.utils.files.Utils.safeStat(filename);
if (!stat) {
delete resources[filename];
}
}
})
);
})
);
await qx.tool.utils.Promisify.poolEachOf(
Object.values(this.__assets),
10,
async asset => {
await asset.load();
let fileInfo = asset.getFileInfo();
if (fileInfo.meta) {
for (var altPath in fileInfo.meta) {
let lib = this.findLibraryForResource(altPath);
if (!lib) {
lib = asset.getLibrary();
}
let otherAsset =
this.__assets[lib.getNamespace() + ":" + altPath];
if (otherAsset) {
otherAsset.addMetaReferee(asset);
asset.addMetaReferTo(otherAsset);
} else {
qx.tool.compiler.Console.warn(
"Cannot find asset " + altPath + " referenced in " + asset
);
}
}
}
if (fileInfo.dependsOn) {
let dependsOn = [];
fileInfo.dependsOn.forEach(str => {
let otherAsset = this.__assets[str];
if (!otherAsset) {
qx.tool.compiler.Console.warn(
"Cannot find asset " + str + " depended on by " + asset
);
} else {
dependsOn.push(otherAsset);
}
});
if (dependsOn.length) {
asset.setDependsOn(dependsOn);
}
}
return null;
}
);
},
/**
* Adds an asset
*
* @param asset {Asset} the asset to add
*/
__addAsset(asset) {
this.__assets[asset.toUri()] = asset;
let library = asset.getLibrary();
let filename = asset.getFilename();
let tmp = "";
filename.split("/").forEach((seg, index) => {
if (index) {
tmp += "/";
}
tmp += seg;
let current = this.__librariesByResourceUri[tmp];
if (current) {
if (qx.lang.Type.isArray(current)) {
if (!qx.lang.Array.contains(current, library)) {
current.push(library);
}
} else if (current !== library) {
current = this.__librariesByResourceUri[tmp] = [current, library];
}
} else {
this.__librariesByResourceUri[tmp] = library;
}
});
asset.setLoaders(
this.__loaders.filter(loader => loader.matches(filename, library))
);
asset.setConverters(
this.__converters.filter(converter =>
converter.matches(filename, library)
)
);
},
/**
* Gets an individual asset
*
* @param srcPath {String} the resource name, with or without a namespace prefix
* @param create {Boolean?} if true the asset will be created if it does not exist
* @param isThemeFile {Boolean?} if true the asset will be expected to be in the theme folder
* @return {Asset?} the asset, if found
*/
getAsset(srcPath, create, isThemeFile) {
let library = this.findLibraryForResource(srcPath);
if (!library) {
qx.tool.compiler.Console.warn("Cannot find library for " + srcPath);
return null;
}
let resourceDir = path.join(
library.getRootDir(),
isThemeFile ? library.getThemePath() : library.getResourcePath()
);
srcPath = path.relative(
resourceDir,
path.isAbsolute(srcPath) ? srcPath : path.join(resourceDir, srcPath)
);
let asset = this.__assets[library.getNamespace() + ":" + srcPath];
if (!asset && create) {
asset = new qx.tool.compiler.resources.Asset(library, srcPath, {
resourcePath: "resourcePath"
});
this.__addAsset(asset);
}
return asset;
},
/**
* Collects information about the assets listed in srcPaths;
*
* @param srcPaths
* @return {Asset[]}
*/
getAssetsForPaths(srcPaths) {
var db = this.__db;
// Generate a lookup that maps the resource name to the meta file that
// contains the composite
var metas = {};
for (var libraryName in db.resources) {
var libraryData = db.resources[libraryName];
for (var resourcePath in libraryData) {
var fileInfo = libraryData[resourcePath];
if (!fileInfo.meta) {
continue;
}
for (var altPath in fileInfo.meta) {
metas[altPath] = resourcePath;
}
}
}
var assets = [];
var assetPaths = {};
srcPaths.forEach(srcPath => {
let pos = srcPath.indexOf(":");
let libraries = null;
if (pos > -1) {
let ns = srcPath.substring(0, pos);
let tmp = this.__analyser.findLibrary(ns);
libraries = tmp ? [tmp] : [];
srcPath = srcPath.substring(pos + 1);
} else {
libraries = this.findLibrariesForResource(srcPath);
}
if (libraries.length == 0) {
qx.tool.compiler.Console.warn("Cannot find library for " + srcPath);
return;
}
libraries.forEach(library => {
let libraryData = db.resources[library.getNamespace()];
pos = srcPath.indexOf("*");
let resourceNames = [];
if (pos > -1) {
srcPath = srcPath.substring(0, pos);
resourceNames = Object.keys(libraryData).filter(
resourceName =>
resourceName.substring(0, srcPath.length) === srcPath
);
} else if (libraryData[srcPath]) {
resourceNames = [srcPath];
}
resourceNames.forEach(resourceName => {
if (assetPaths[resourceName] !== undefined) {
return;
}
let asset =
this.__assets[library.getNamespace() + ":" + resourceName];
let fileInfo = asset.getFileInfo();
if (fileInfo.doNotCopy === true) {
return;
}
(asset.getMetaReferees() || []).forEach(meta => {
// Extract the fragment from the meta data for this particular resource
var resMetaData = meta.getFileInfo().meta[resourceName];
fileInfo.composite = resMetaData[3];
fileInfo.x = resMetaData[4];
fileInfo.y = resMetaData[5];
});
assets.push(asset);
assetPaths[resourceName] = assets.length - 1;
});
});
});
return assets;
}
}
});