更新
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
||||
class AbstractFileManager {
|
||||
getPath(filename) {
|
||||
let j = filename.lastIndexOf('?');
|
||||
if (j > 0) {
|
||||
filename = filename.slice(0, j);
|
||||
}
|
||||
j = filename.lastIndexOf('/');
|
||||
if (j < 0) {
|
||||
j = filename.lastIndexOf('\\');
|
||||
}
|
||||
if (j < 0) {
|
||||
return '';
|
||||
}
|
||||
return filename.slice(0, j + 1);
|
||||
}
|
||||
|
||||
tryAppendExtension(path, ext) {
|
||||
return /(\.[a-z]*$)|([?;].*)$/.test(path) ? path : path + ext;
|
||||
}
|
||||
|
||||
tryAppendLessExtension(path) {
|
||||
return this.tryAppendExtension(path, '.less');
|
||||
}
|
||||
|
||||
supportsSync() {
|
||||
return false;
|
||||
}
|
||||
|
||||
alwaysMakePathsAbsolute() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isPathAbsolute(filename) {
|
||||
return (/^(?:[a-z-]+:|\/|\\|#)/i).test(filename);
|
||||
}
|
||||
|
||||
// TODO: pull out / replace?
|
||||
join(basePath, laterPath) {
|
||||
if (!basePath) {
|
||||
return laterPath;
|
||||
}
|
||||
return basePath + laterPath;
|
||||
}
|
||||
|
||||
pathDiff(url, baseUrl) {
|
||||
// diff between two paths to create a relative path
|
||||
|
||||
const urlParts = this.extractUrlParts(url);
|
||||
|
||||
const baseUrlParts = this.extractUrlParts(baseUrl);
|
||||
let i;
|
||||
let max;
|
||||
let urlDirectories;
|
||||
let baseUrlDirectories;
|
||||
let diff = '';
|
||||
if (urlParts.hostPart !== baseUrlParts.hostPart) {
|
||||
return '';
|
||||
}
|
||||
max = Math.max(baseUrlParts.directories.length, urlParts.directories.length);
|
||||
for (i = 0; i < max; i++) {
|
||||
if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; }
|
||||
}
|
||||
baseUrlDirectories = baseUrlParts.directories.slice(i);
|
||||
urlDirectories = urlParts.directories.slice(i);
|
||||
for (i = 0; i < baseUrlDirectories.length - 1; i++) {
|
||||
diff += '../';
|
||||
}
|
||||
for (i = 0; i < urlDirectories.length - 1; i++) {
|
||||
diff += `${urlDirectories[i]}/`;
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function, not part of API.
|
||||
* This should be replaceable by newer Node / Browser APIs
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} baseUrl
|
||||
*/
|
||||
extractUrlParts(url, baseUrl) {
|
||||
// urlParts[1] = protocol://hostname/ OR /
|
||||
// urlParts[2] = / if path relative to host base
|
||||
// urlParts[3] = directories
|
||||
// urlParts[4] = filename
|
||||
// urlParts[5] = parameters
|
||||
|
||||
const urlPartsRegex = /^((?:[a-z-]+:)?\/{2}(?:[^/?#]*\/)|([/\\]))?((?:[^/\\?#]*[/\\])*)([^/\\?#]*)([#?].*)?$/i;
|
||||
|
||||
const urlParts = url.match(urlPartsRegex);
|
||||
const returner = {};
|
||||
let rawDirectories = [];
|
||||
const directories = [];
|
||||
let i;
|
||||
let baseUrlParts;
|
||||
|
||||
if (!urlParts) {
|
||||
throw new Error(`Could not parse sheet href - '${url}'`);
|
||||
}
|
||||
|
||||
// Stylesheets in IE don't always return the full path
|
||||
if (baseUrl && (!urlParts[1] || urlParts[2])) {
|
||||
baseUrlParts = baseUrl.match(urlPartsRegex);
|
||||
if (!baseUrlParts) {
|
||||
throw new Error(`Could not parse page url - '${baseUrl}'`);
|
||||
}
|
||||
urlParts[1] = urlParts[1] || baseUrlParts[1] || '';
|
||||
if (!urlParts[2]) {
|
||||
urlParts[3] = baseUrlParts[3] + urlParts[3];
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParts[3]) {
|
||||
rawDirectories = urlParts[3].replace(/\\/g, '/').split('/');
|
||||
|
||||
// collapse '..' and skip '.'
|
||||
for (i = 0; i < rawDirectories.length; i++) {
|
||||
|
||||
if (rawDirectories[i] === '..') {
|
||||
directories.pop();
|
||||
}
|
||||
else if (rawDirectories[i] !== '.') {
|
||||
directories.push(rawDirectories[i]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
returner.hostPart = urlParts[1];
|
||||
returner.directories = directories;
|
||||
returner.rawPath = (urlParts[1] || '') + rawDirectories.join('/');
|
||||
returner.path = (urlParts[1] || '') + directories.join('/');
|
||||
returner.filename = urlParts[4];
|
||||
returner.fileUrl = returner.path + (urlParts[4] || '');
|
||||
returner.url = returner.fileUrl + (urlParts[5] || '');
|
||||
return returner;
|
||||
}
|
||||
}
|
||||
|
||||
export default AbstractFileManager;
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import functionRegistry from '../functions/function-registry.js';
|
||||
import LessError from '../less-error.js';
|
||||
|
||||
class AbstractPluginLoader {
|
||||
constructor() {
|
||||
// Implemented by Node.js plugin loader
|
||||
this.require = function() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
evalPlugin(contents, context, imports, pluginOptions, fileInfo) {
|
||||
|
||||
let loader, registry, pluginObj, localModule, pluginManager, filename, result;
|
||||
|
||||
pluginManager = context.pluginManager;
|
||||
|
||||
if (fileInfo) {
|
||||
if (typeof fileInfo === 'string') {
|
||||
filename = fileInfo;
|
||||
}
|
||||
else {
|
||||
filename = fileInfo.filename;
|
||||
}
|
||||
}
|
||||
const shortname = (new this.less.FileManager()).extractUrlParts(filename).filename;
|
||||
|
||||
if (filename) {
|
||||
pluginObj = pluginManager.get(filename);
|
||||
|
||||
if (pluginObj) {
|
||||
result = this.trySetOptions(pluginObj, filename, shortname, pluginOptions);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
if (pluginObj.use) {
|
||||
pluginObj.use.call(this.context, pluginObj);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
e.message = e.message || 'Error during @plugin call';
|
||||
return new LessError(e, imports, filename);
|
||||
}
|
||||
return pluginObj;
|
||||
}
|
||||
}
|
||||
localModule = {
|
||||
exports: {},
|
||||
pluginManager,
|
||||
fileInfo
|
||||
};
|
||||
registry = functionRegistry.create();
|
||||
|
||||
const registerPlugin = function(obj) {
|
||||
pluginObj = obj;
|
||||
};
|
||||
|
||||
try {
|
||||
loader = new Function('module', 'require', 'registerPlugin', 'functions', 'tree', 'less', 'fileInfo', contents);
|
||||
loader(localModule, this.require(filename), registerPlugin, registry, this.less.tree, this.less, fileInfo);
|
||||
}
|
||||
catch (e) {
|
||||
return new LessError(e, imports, filename);
|
||||
}
|
||||
|
||||
if (!pluginObj) {
|
||||
pluginObj = localModule.exports;
|
||||
}
|
||||
pluginObj = this.validatePlugin(pluginObj, filename, shortname);
|
||||
|
||||
if (pluginObj instanceof LessError) {
|
||||
return pluginObj;
|
||||
}
|
||||
|
||||
if (pluginObj) {
|
||||
pluginObj.imports = imports;
|
||||
pluginObj.filename = filename;
|
||||
|
||||
// For < 3.x (or unspecified minVersion) - setOptions() before install()
|
||||
if (!pluginObj.minVersion || this.compareVersion('3.0.0', pluginObj.minVersion) < 0) {
|
||||
result = this.trySetOptions(pluginObj, filename, shortname, pluginOptions);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Run on first load
|
||||
pluginManager.addPlugin(pluginObj, fileInfo.filename, registry);
|
||||
pluginObj.functions = registry.getLocalFunctions();
|
||||
|
||||
// Need to call setOptions again because the pluginObj might have functions
|
||||
result = this.trySetOptions(pluginObj, filename, shortname, pluginOptions);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Run every @plugin call
|
||||
try {
|
||||
if (pluginObj.use) {
|
||||
pluginObj.use.call(this.context, pluginObj);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
e.message = e.message || 'Error during @plugin call';
|
||||
return new LessError(e, imports, filename);
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
return new LessError({ message: 'Not a valid plugin' }, imports, filename);
|
||||
}
|
||||
|
||||
return pluginObj;
|
||||
|
||||
}
|
||||
|
||||
trySetOptions(plugin, filename, name, options) {
|
||||
if (options && !plugin.setOptions) {
|
||||
return new LessError({
|
||||
message: `Options have been provided but the plugin ${name} does not support any options.`
|
||||
});
|
||||
}
|
||||
try {
|
||||
plugin.setOptions && plugin.setOptions(options);
|
||||
}
|
||||
catch (e) {
|
||||
return new LessError(e);
|
||||
}
|
||||
}
|
||||
|
||||
validatePlugin(plugin, filename, name) {
|
||||
if (plugin) {
|
||||
// support plugins being a function
|
||||
// so that the plugin can be more usable programmatically
|
||||
if (typeof plugin === 'function') {
|
||||
plugin = new plugin();
|
||||
}
|
||||
|
||||
if (plugin.minVersion) {
|
||||
if (this.compareVersion(plugin.minVersion, this.less.version) < 0) {
|
||||
return new LessError({
|
||||
message: `Plugin ${name} requires version ${this.versionToString(plugin.minVersion)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
compareVersion(aVersion, bVersion) {
|
||||
if (typeof aVersion === 'string') {
|
||||
aVersion = aVersion.match(/^(\d+)\.?(\d+)?\.?(\d+)?/);
|
||||
aVersion.shift();
|
||||
}
|
||||
for (let i = 0; i < aVersion.length; i++) {
|
||||
if (aVersion[i] !== bVersion[i]) {
|
||||
return parseInt(aVersion[i]) > parseInt(bVersion[i]) ? -1 : 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
versionToString(version) {
|
||||
let versionString = '';
|
||||
for (let i = 0; i < version.length; i++) {
|
||||
versionString += (versionString ? '.' : '') + version[i];
|
||||
}
|
||||
return versionString;
|
||||
}
|
||||
|
||||
printUsage(plugins) {
|
||||
for (let i = 0; i < plugins.length; i++) {
|
||||
const plugin = plugins[i];
|
||||
if (plugin.printUsage) {
|
||||
plugin.printUsage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AbstractPluginLoader;
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
export interface Environment {
|
||||
/**
|
||||
* Converts a string to a base 64 string
|
||||
*/
|
||||
encodeBase64(str: string): string
|
||||
/**
|
||||
* Lookup the mime-type of a filename
|
||||
*/
|
||||
mimeLookup(filename: string): string
|
||||
/**
|
||||
* Look up the charset of a mime type
|
||||
* @param mime
|
||||
*/
|
||||
charsetLookup(mime: string): string
|
||||
/**
|
||||
* Gets a source map generator
|
||||
*
|
||||
* @todo - Figure out precise type
|
||||
*/
|
||||
getSourceMapGenerator(): any
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @todo Document why this abstraction exists, and the relationship between
|
||||
* environment, file managers, and plugin manager
|
||||
*/
|
||||
|
||||
import logger from '../logger.js';
|
||||
|
||||
class Environment {
|
||||
constructor(externalEnvironment, fileManagers) {
|
||||
this.fileManagers = fileManagers || [];
|
||||
externalEnvironment = externalEnvironment || {};
|
||||
|
||||
const optionalFunctions = ['encodeBase64', 'mimeLookup', 'charsetLookup', 'getSourceMapGenerator'];
|
||||
const requiredFunctions = [];
|
||||
const functions = requiredFunctions.concat(optionalFunctions);
|
||||
|
||||
for (let i = 0; i < functions.length; i++) {
|
||||
const propName = functions[i];
|
||||
const environmentFunc = externalEnvironment[propName];
|
||||
if (environmentFunc) {
|
||||
this[propName] = environmentFunc.bind(externalEnvironment);
|
||||
} else if (i < requiredFunctions.length) {
|
||||
this.warn(`missing required function in environment - ${propName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFileManager(filename, currentDirectory, options, environment, isSync) {
|
||||
|
||||
if (!filename) {
|
||||
logger.warn('getFileManager called with no filename.. Please report this issue. continuing.');
|
||||
}
|
||||
if (currentDirectory === undefined) {
|
||||
logger.warn('getFileManager called with null directory.. Please report this issue. continuing.');
|
||||
}
|
||||
|
||||
let fileManagers = this.fileManagers;
|
||||
if (options.pluginManager) {
|
||||
fileManagers = [].concat(fileManagers).concat(options.pluginManager.getFileManagers());
|
||||
}
|
||||
for (let i = fileManagers.length - 1; i >= 0 ; i--) {
|
||||
const fileManager = fileManagers[i];
|
||||
if (fileManager[isSync ? 'supportsSync' : 'supports'](filename, currentDirectory, options, environment)) {
|
||||
return fileManager;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
addFileManager(fileManager) {
|
||||
this.fileManagers.push(fileManager);
|
||||
}
|
||||
|
||||
clearFileManagers() {
|
||||
this.fileManagers = [];
|
||||
}
|
||||
}
|
||||
|
||||
export default Environment;
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import type { Environment } from './environment-api'
|
||||
|
||||
export interface FileManager {
|
||||
/**
|
||||
* Given the full path to a file, return the path component
|
||||
* Provided by AbstractFileManager
|
||||
*/
|
||||
getPath(filename: string): string
|
||||
/**
|
||||
* Append a .less extension if appropriate. Only called if less thinks one could be added.
|
||||
* Provided by AbstractFileManager
|
||||
*/
|
||||
tryAppendLessExtension(filename: string): string
|
||||
/**
|
||||
* Whether the rootpath should be converted to be absolute.
|
||||
* The browser ovverides this to return true because urls must be absolute.
|
||||
* Provided by AbstractFileManager (returns false)
|
||||
*/
|
||||
alwaysMakePathsAbsolute(): boolean
|
||||
/**
|
||||
* Returns whether a path is absolute
|
||||
* Provided by AbstractFileManager
|
||||
*/
|
||||
isPathAbsolute(path: string): boolean
|
||||
/**
|
||||
* joins together 2 paths
|
||||
* Provided by AbstractFileManager
|
||||
*/
|
||||
join(basePath: string, laterPath: string): string
|
||||
/**
|
||||
* Returns the difference between 2 paths
|
||||
* E.g. url = a/ baseUrl = a/b/ returns ../
|
||||
* url = a/b/ baseUrl = a/ returns b/
|
||||
* Provided by AbstractFileManager
|
||||
*/
|
||||
pathDiff(url: string, baseUrl: string): string
|
||||
/**
|
||||
* Returns whether this file manager supports this file for syncronous file retrieval
|
||||
* If true is returned, loadFileSync will then be called with the file.
|
||||
* Provided by AbstractFileManager (returns false)
|
||||
*
|
||||
* @todo - Narrow Options type
|
||||
*/
|
||||
supportsSync(
|
||||
filename: string,
|
||||
currentDirectory: string,
|
||||
options: Record<string, any>,
|
||||
environment: Environment
|
||||
): boolean
|
||||
/**
|
||||
* If file manager supports async file retrieval for this file type
|
||||
*/
|
||||
supports(
|
||||
filename: string,
|
||||
currentDirectory: string,
|
||||
options: Record<string, any>,
|
||||
environment: Environment
|
||||
): boolean
|
||||
/**
|
||||
* Loads a file asynchronously.
|
||||
*/
|
||||
loadFile(
|
||||
filename: string,
|
||||
currentDirectory: string,
|
||||
options: Record<string, any>,
|
||||
environment: Environment
|
||||
): Promise<{ filename: string, contents: string }>
|
||||
/**
|
||||
* Loads a file synchronously. Expects an immediate return with an object
|
||||
*/
|
||||
loadFileSync(
|
||||
filename: string,
|
||||
currentDirectory: string,
|
||||
options: Record<string, any>,
|
||||
environment: Environment
|
||||
): { error?: unknown, filename: string, contents: string }
|
||||
}
|
||||
Reference in New Issue
Block a user