|
| 1 | +const path = require("path"); |
| 2 | +const createParser = require("./less/parser"); |
| 3 | +const clone = require("clone"); |
| 4 | + |
| 5 | +// Plugins |
| 6 | +const ImportCollectorPlugin = require("./plugin/import-collector"); |
| 7 | +const VariableCollectorPlugin = require("./plugin/variable-collector"); |
| 8 | +const UrlCollector = require("./plugin/url-collector"); |
| 9 | + |
| 10 | +class Compiler { |
| 11 | + constructor({options, fileUtils, customFs}) { |
| 12 | + this.options = options; |
| 13 | + this.fileUtils = fileUtils; |
| 14 | + |
| 15 | + // Stores mapping between "virtual" paths (used within LESS) and real filesystem paths. |
| 16 | + // Only relevant when using the "rootPaths" option. |
| 17 | + this.mFileMappings = {}; |
| 18 | + |
| 19 | + // Only use custom file handler when "rootPaths" or custom "fs" are used |
| 20 | + if ((this.options.rootPaths && this.options.rootPaths.length > 0) || customFs) { |
| 21 | + this.fnFileHandler = this.createFileHandler(); |
| 22 | + } else { |
| 23 | + this.fnFileHandler = undefined; |
| 24 | + } |
| 25 | + } |
| 26 | + createFileHandler() { |
| 27 | + return async (file, currentFileInfo, handleDataAndCallCallback, callback) => { |
| 28 | + try { |
| 29 | + let pathname; |
| 30 | + |
| 31 | + // support absolute paths such as "/resources/my/base.less" |
| 32 | + if (path.posix.isAbsolute(file)) { |
| 33 | + pathname = path.posix.normalize(file); |
| 34 | + } else { |
| 35 | + pathname = path.posix.join(currentFileInfo.currentDirectory, file); |
| 36 | + } |
| 37 | + |
| 38 | + const result = await this.fileUtils.readFile(pathname, this.options.rootPaths); |
| 39 | + if (!result) { |
| 40 | + // eslint-disable-next-line no-console |
| 41 | + console.log("File not found: " + pathname); |
| 42 | + callback({type: "File", message: "Could not find file at path '" + pathname + "'"}); |
| 43 | + } else { |
| 44 | + try { |
| 45 | + // Save import mapping to calculate full import paths later on |
| 46 | + this.mFileMappings[currentFileInfo.rootFilename] = |
| 47 | + this.mFileMappings[currentFileInfo.rootFilename] || {}; |
| 48 | + this.mFileMappings[currentFileInfo.rootFilename][pathname] = result.path; |
| 49 | + |
| 50 | + handleDataAndCallCallback(pathname, result.content); |
| 51 | + } catch (e) { |
| 52 | + // eslint-disable-next-line no-console |
| 53 | + console.log(e); |
| 54 | + callback(e); |
| 55 | + } |
| 56 | + } |
| 57 | + } catch (err) { |
| 58 | + // eslint-disable-next-line no-console |
| 59 | + console.log(err); |
| 60 | + callback(err); |
| 61 | + } |
| 62 | + }; |
| 63 | + } |
| 64 | + async compile(config) { |
| 65 | + const parserOptions = clone(this.options.parser); |
| 66 | + let rootpath; |
| 67 | + |
| 68 | + if (config.path) { |
| 69 | + // Keep rootpath for later usage in the ImportCollectorPlugin |
| 70 | + rootpath = config.path; |
| 71 | + parserOptions.filename = config.localPath; |
| 72 | + } |
| 73 | + |
| 74 | + // inject the library name as prefix (e.g. "sap.m" as "_sap_m") |
| 75 | + if (this.options.library && typeof this.options.library.name === "string") { |
| 76 | + const libName = config.libName = this.options.library.name; |
| 77 | + config.libPath = libName.replace(/\./g, "/"); |
| 78 | + config.prefix = "_" + libName.replace(/\./g, "_") + "_"; |
| 79 | + } else { |
| 80 | + config.prefix = ""; // no library, no prefix |
| 81 | + } |
| 82 | + |
| 83 | + // Keep filename for later usage (see ImportCollectorPlugin) as less modifies the parserOptions.filename |
| 84 | + const filename = parserOptions.filename; |
| 85 | + |
| 86 | + const parser = createParser(parserOptions, this.fnFileHandler); |
| 87 | + |
| 88 | + function parseContent(content) { |
| 89 | + return new Promise(function(resolve, reject) { |
| 90 | + parser.parse(content, function(err, tree) { |
| 91 | + if (err) { |
| 92 | + reject(err); |
| 93 | + } else { |
| 94 | + resolve(tree); |
| 95 | + } |
| 96 | + }); |
| 97 | + }); |
| 98 | + } |
| 99 | + |
| 100 | + const tree = await parseContent(config.content); |
| 101 | + const result = {tree}; |
| 102 | + |
| 103 | + // plugins to collect imported files and variable values |
| 104 | + const oImportCollector = new ImportCollectorPlugin({ |
| 105 | + importMappings: this.mFileMappings[filename] |
| 106 | + }); |
| 107 | + const oVariableCollector = new VariableCollectorPlugin(this.options.compiler); |
| 108 | + const oUrlCollector = new UrlCollector(); |
| 109 | + |
| 110 | + // render to css |
| 111 | + result.css = tree.toCSS(Object.assign({}, this.options.compiler, { |
| 112 | + plugins: [oImportCollector, oVariableCollector, oUrlCollector] |
| 113 | + })); |
| 114 | + |
| 115 | + // retrieve imported files |
| 116 | + result.imports = oImportCollector.getImports(); |
| 117 | + |
| 118 | + // retrieve reduced set of variables |
| 119 | + result.variables = oVariableCollector.getVariables(Object.keys(this.mFileMappings[filename] || {})); |
| 120 | + |
| 121 | + // retrieve all variables |
| 122 | + result.allVariables = oVariableCollector.getAllVariables(); |
| 123 | + |
| 124 | + // also compile rtl-version if requested |
| 125 | + let oRTL; |
| 126 | + if (this.options.rtl) { |
| 127 | + const RTLPlugin = require("./plugin/rtl"); |
| 128 | + oRTL = new RTLPlugin(); |
| 129 | + |
| 130 | + const urls = oUrlCollector.getUrls(); |
| 131 | + |
| 132 | + const existingImgRtlUrls = (await Promise.all( |
| 133 | + urls.map(async ({currentDirectory, relativeUrl}) => { |
| 134 | + const relativeImgRtlUrl = RTLPlugin.getRtlImgUrl(relativeUrl); |
| 135 | + if (relativeImgRtlUrl) { |
| 136 | + const resolvedImgRtlUrl = path.posix.join(currentDirectory, relativeImgRtlUrl); |
| 137 | + if (await this.fileUtils.findFile(resolvedImgRtlUrl, this.options.rootPaths)) { |
| 138 | + return resolvedImgRtlUrl; |
| 139 | + } |
| 140 | + } |
| 141 | + }) |
| 142 | + )).filter(Boolean); |
| 143 | + |
| 144 | + oRTL.setExistingImgRtlPaths(existingImgRtlUrls); |
| 145 | + } |
| 146 | + |
| 147 | + if (oRTL) { |
| 148 | + result.cssRtl = tree.toCSS(Object.assign({}, this.options.compiler, { |
| 149 | + plugins: [oRTL] |
| 150 | + })); |
| 151 | + } |
| 152 | + |
| 153 | + if (rootpath) { |
| 154 | + result.imports.unshift(rootpath); |
| 155 | + } |
| 156 | + |
| 157 | + // also compile css-variables version if requested |
| 158 | + if (this.options.cssVariables) { |
| 159 | + // parse the content again to have a clean tree |
| 160 | + const cssVariablesSkeletonTree = await parseContent(config.content); |
| 161 | + |
| 162 | + // generate the skeleton-css and the less-variables |
| 163 | + const CSSVariablesCollectorPlugin = require("./plugin/css-variables-collector"); |
| 164 | + const oCSSVariablesCollector = new CSSVariablesCollectorPlugin(config); |
| 165 | + const oVariableCollector = new VariableCollectorPlugin(this.options.compiler); |
| 166 | + result.cssSkeleton = cssVariablesSkeletonTree.toCSS(Object.assign({}, this.options.compiler, { |
| 167 | + plugins: [oCSSVariablesCollector, oVariableCollector] |
| 168 | + })); |
| 169 | + const varsOverride = oVariableCollector.getAllVariables(); |
| 170 | + result.cssVariablesSource = oCSSVariablesCollector.toLessVariables(varsOverride); |
| 171 | + |
| 172 | + if (oRTL) { |
| 173 | + const oCSSVariablesCollectorRTL = new CSSVariablesCollectorPlugin(config); |
| 174 | + result.cssSkeletonRtl = cssVariablesSkeletonTree.toCSS(Object.assign({}, this.options.compiler, { |
| 175 | + plugins: [oCSSVariablesCollectorRTL, oRTL] |
| 176 | + })); |
| 177 | + } |
| 178 | + |
| 179 | + // generate the css-variables content out of the less-variables |
| 180 | + const cssVariablesTree = await parseContent(result.cssVariablesSource); |
| 181 | + const CSSVariablesPointerPlugin = require("./plugin/css-variables-pointer"); |
| 182 | + result.cssVariables = cssVariablesTree.toCSS(Object.assign({}, this.options.compiler, { |
| 183 | + plugins: [new CSSVariablesPointerPlugin()] |
| 184 | + })); |
| 185 | + } |
| 186 | + |
| 187 | + return result; |
| 188 | + } |
| 189 | +} |
| 190 | + |
| 191 | +module.exports = Compiler; |
0 commit comments