UNPKG

@ui5/project

Version:
542 lines (507 loc) 17.6 kB
import fsPath from "node:path"; import posixPath from "node:path/posix"; import {promisify} from "node:util"; import ComponentProject from "../ComponentProject.js"; import * as resourceFactory from "@ui5/fs/resourceFactory"; /** * Library * * @public * @class * @alias @ui5/project/specifications/types/Library * @extends @ui5/project/specifications/ComponentProject * @hideconstructor */ class Library extends ComponentProject { constructor(parameters) { super(parameters); this._pManifest = null; this._pDotLibrary = null; this._pLibraryJs = null; this._srcPath = "src"; this._testPath = "test"; this._testPathExists = false; this._isSourceNamespaced = true; this._propertiesFilesSourceEncoding = "UTF-8"; } /* === Attributes === */ /** * * @private */ getLibraryPreloadExcludes() { return this._config.builder && this._config.builder.libraryPreload && this._config.builder.libraryPreload.excludes || []; } /** * @private */ getJsdocExcludes() { return this._config.builder && this._config.builder.jsdoc && this._config.builder.jsdoc.excludes || []; } /** * Get the path of the project's source directory. This might not be POSIX-style on some platforms. * * @public * @returns {string} Absolute path to the source directory of the project */ getSourcePath() { return fsPath.join(this.getRootPath(), this._srcPath); } /* === Resource Access === */ /** * Get a resource reader for the sources of the project (excluding any test resources) * * @param {string[]} excludes List of glob patterns to exclude * @returns {@ui5/fs/ReaderCollection} Reader collection */ _getSourceReader(excludes) { // TODO: Throw for libraries with additional namespaces like sap.ui.core? let virBasePath = "/resources/"; if (!this._isSourceNamespaced) { // In case the namespace is not represented in the source directory // structure, add it to the virtual base path virBasePath += `${this._namespace}/`; } return resourceFactory.createReader({ fsBasePath: this.getSourcePath(), virBasePath, name: `Source reader for library project ${this.getName()}`, project: this, excludes }); } /** * Get a resource reader for the test-resources of the project * * @param {string[]} excludes List of glob patterns to exclude * @returns {@ui5/fs/ReaderCollection} Reader collection */ _getTestReader(excludes) { if (!this._testPathExists) { return null; } let virBasePath = "/test-resources/"; if (!this._isSourceNamespaced) { // In case the namespace is not represented in the source directory // structure, add it to the virtual base path virBasePath += `${this._namespace}/`; } const testReader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getRootPath(), this._testPath), virBasePath, name: `Runtime test-resources reader for library project ${this.getName()}`, project: this, excludes }); return testReader; } /** * Get a resource reader for the sources of the project (excluding any test resources) * without a virtual base path. * In the future the path structure can be flat or namespaced depending on the project * setup * * @returns {@ui5/fs/ReaderCollection} Reader collection */ _getRawSourceReader() { return resourceFactory.createReader({ fsBasePath: this.getSourcePath(), virBasePath: "/", name: `Raw source reader for library project ${this.getName()}`, project: this }); } /* === Internals === */ /** * @private * @param {object} config Configuration object */ async _configureAndValidatePaths(config) { await super._configureAndValidatePaths(config); if (config.resources && config.resources.configuration && config.resources.configuration.paths) { if (config.resources.configuration.paths.src) { this._srcPath = config.resources.configuration.paths.src; } if (config.resources.configuration.paths.test) { this._testPath = config.resources.configuration.paths.test; } } if (!(await this._dirExists("/" + this._srcPath))) { throw new Error( `Unable to find source directory '${this._srcPath}' in library project ${this.getName()}`); } this._testPathExists = await this._dirExists("/" + this._testPath); this._log.verbose(`Path mapping for library project ${this.getName()}:`); this._log.verbose(` Physical root path: ${this.getRootPath()}`); this._log.verbose(` Mapped to:`); this._log.verbose(` /resources/ => ${this._srcPath}`); this._log.verbose( ` /test-resources/ => ${this._testPath}${this._testPathExists ? "" : " [does not exist]"}`); } /** * @private * @param {object} config Configuration object * @param {object} buildDescription Cache metadata object */ async _parseConfiguration(config, buildDescription) { await super._parseConfiguration(config, buildDescription); if (buildDescription) { this._namespace = buildDescription.namespace; return; } this._namespace = await this._getNamespace(); if (!config.metadata.copyright) { const copyright = await this._getCopyrightFromDotLibrary(); if (copyright) { config.metadata.copyright = copyright; } } if (this.isFrameworkProject()) { // Only framework projects are allowed to provide preload-excludes in their .library file, // and only if it is not already defined in the ui5.yaml if (config.builder?.libraryPreload?.excludes) { this._log.verbose( `Using preload excludes for framework library ${this.getName()} from project configuration`); } else { this._log.verbose( `No preload excludes defined in project configuration of framework library ` + `${this.getName()}. Falling back to .library...`); const excludes = await this._getPreloadExcludesFromDotLibrary(); if (excludes) { if (!config.builder) { config.builder = {}; } if (!config.builder.libraryPreload) { config.builder.libraryPreload = {}; } config.builder.libraryPreload.excludes = excludes; } } } } /** * Determine library namespace by checking manifest.json with fallback to .library. * Any maven placeholders are resolved from the projects pom.xml * * @returns {string} Namespace of the project * @throws {Error} if namespace can not be determined */ async _getNamespace() { // Trigger both reads asynchronously const [{ namespace: manifestNs, filePath: manifestPath }, { namespace: dotLibraryNs, filePath: dotLibraryPath }] = await Promise.all([ this._getNamespaceFromManifest(), this._getNamespaceFromDotLibrary() ]); let libraryNs; let namespacePath; if (manifestNs && dotLibraryNs) { // Both files present // => check whether they are on the same level const manifestDepth = manifestPath.split("/").length; const dotLibraryDepth = dotLibraryPath.split("/").length; if (manifestDepth < dotLibraryDepth) { // We see the .library file as the "leading" file of a library // Therefore, a manifest.json on a higher level is something we do not except throw new Error(`Failed to detect namespace for project ${this.getName()}: ` + `Found a manifest.json on a higher directory level than the .library file. ` + `It should be on the same or a lower level. ` + `Note that a manifest.json on a lower level will be ignored.\n` + ` manifest.json path: ${manifestPath}\n` + ` is higher than\n` + ` .library path: ${dotLibraryPath}`); } if (manifestDepth === dotLibraryDepth) { if (posixPath.dirname(manifestPath) !== posixPath.dirname(dotLibraryPath)) { // This just should not happen in your project throw new Error(`Failed to detect namespace for project ${this.getName()}: ` + `Found a manifest.json on the same directory level but in a different directory ` + `than the .library file. They should be in the same directory.\n` + ` manifest.json path: ${manifestPath}\n` + ` is different to\n` + ` .library path: ${dotLibraryPath}`); } // Typical scenario if both files are present this._log.verbose( `Found a manifest.json and a .library file on the same level for ` + `project ${this.getName()}.`); this._log.verbose( `Resolving namespace of project ${this.getName()} from manifest.json...`); libraryNs = manifestNs; namespacePath = posixPath.dirname(manifestPath); } else { // Typical scenario: Some nested component has a manifest.json but the library itself only // features a .library. => Ignore the manifest.json this._log.verbose( `Ignoring manifest.json found on a lower level than the .library file of ` + `project ${this.getName()}.`); this._log.verbose( `Resolving namespace of project ${this.getName()} from .library...`); libraryNs = dotLibraryNs; namespacePath = posixPath.dirname(dotLibraryPath); } } else if (manifestNs) { // Only manifest available this._log.verbose( `Resolving namespace of project ${this.getName()} from manifest.json...`); libraryNs = manifestNs; namespacePath = posixPath.dirname(manifestPath); } else if (dotLibraryNs) { // Only .library available this._log.verbose( `Resolving namespace of project ${this.getName()} from .library...`); libraryNs = dotLibraryNs; namespacePath = posixPath.dirname(dotLibraryPath); } else { this._log.verbose( `Failed to resolve namespace of project ${this.getName()} from manifest.json ` + `or .library file. Falling back to library.js file path...`); } let namespace; if (libraryNs) { // Maven placeholders can only exist in manifest.json or .library configuration if (this._hasMavenPlaceholder(libraryNs)) { try { libraryNs = await this._resolveMavenPlaceholder(libraryNs); } catch (err) { throw new Error( `Failed to resolve namespace maven placeholder of project ` + `${this.getName()}: ${err.message}`); } } namespace = libraryNs.replace(/\./g, "/"); if (namespacePath === "/") { this._log.verbose(`Detected flat library source structure for project ${this.getName()}`); this._isSourceNamespaced = false; } else { namespacePath = namespacePath.replace("/", ""); // remove leading slash if (namespacePath !== namespace) { throw new Error( `Detected namespace "${namespace}" does not match detected directory ` + `structure "${namespacePath}" for project ${this.getName()}`); } } } else { try { const libraryJsPath = await this._getLibraryJsPath(); namespacePath = posixPath.dirname(libraryJsPath); namespace = namespacePath.replace("/", ""); // remove leading slash if (namespace === "") { throw new Error(`Found library.js file in root directory. ` + `Expected it to be in namespace directory.`); } this._log.verbose( `Deriving namespace for project ${this.getName()} from ` + `path of library.js file`); } catch (err) { this._log.verbose( `Namespace resolution from library.js file path failed for project ` + `${this.getName()}: ${err.message}`); } } if (!namespace) { throw new Error(`Failed to detect namespace or namespace is empty for ` + `project ${this.getName()}. Check verbose log for details.`); } this._log.verbose( `Namespace of project ${this.getName()} is ${namespace}`); return namespace; } async _getNamespaceFromManifest() { try { const {content: manifest, filePath} = await this._getManifest(); // check for a proper sap.app/id in manifest.json to determine namespace if (manifest["sap.app"] && manifest["sap.app"].id) { const namespace = manifest["sap.app"].id; this._log.verbose( `Found namespace ${namespace} in manifest.json of project ${this.getName()} ` + `at ${filePath}`); return { namespace, filePath }; } else { throw new Error( `No sap.app/id configuration found in manifest.json of project ${this.getName()} ` + `at ${filePath}`); } } catch (err) { this._log.verbose( `Namespace resolution from manifest.json failed for project ` + `${this.getName()}: ${err.message}`); } return {}; } async _getNamespaceFromDotLibrary() { try { const {content: dotLibrary, filePath} = await this._getDotLibrary(); const namespace = dotLibrary?.library?.name?._; if (namespace) { this._log.verbose( `Found namespace ${namespace} in .library file of project ${this.getName()} ` + `at ${filePath}`); return { namespace, filePath }; } else { throw new Error( `No library name found in .library of project ${this.getName()} ` + `at ${filePath}`); } } catch (err) { this._log.verbose( `Namespace resolution from .library failed for project ` + `${this.getName()}: ${err.message}`); } return {}; } /** * Determines library copyright from given project configuration with fallback to .library. * * @returns {string|null} Copyright of the project */ async _getCopyrightFromDotLibrary() { try { // If no copyright replacement was provided by ui5.yaml, // check if the .library file has a valid copyright replacement const {content: dotLibrary, filePath} = await this._getDotLibrary(); if (dotLibrary?.library?.copyright?._) { this._log.verbose( `Using copyright from ${filePath} for project ${this.getName()}...`); return dotLibrary.library.copyright._; } else { this._log.verbose( `No copyright configuration found in ${filePath} ` + `of project ${this.getName()}`); return null; } } catch (err) { this._log.verbose( `Copyright determination from .library failed for project ` + `${this.getName()}: ${err.message}`); return null; } } async _getPreloadExcludesFromDotLibrary() { const {content: dotLibrary, filePath} = await this._getDotLibrary(); let excludes = dotLibrary?.library?.appData?.packaging?.["all-in-one"]?.exclude; if (excludes) { if (!Array.isArray(excludes)) { excludes = [excludes]; } this._log.verbose( `Found ${excludes.length} preload excludes in .library file of ` + `project ${this.getName()} at ${filePath}`); return excludes.map((exclude) => { return exclude.$.name; }); } else { this._log.verbose( `No preload excludes found in .library of project ${this.getName()} ` + `at ${filePath}`); return null; } } /** * Reads the projects manifest.json * * @returns {Promise<object>} resolves with an object containing the <code>content</code> (as JSON) and * <code>filePath</code> (as string) of the manifest.json file */ async _getManifest() { if (this._pManifest) { return this._pManifest; } return this._pManifest = this._getRawSourceReader().byGlob("**/manifest.json") .then(async (manifestResources) => { if (!manifestResources.length) { throw new Error(`Could not find manifest.json file for project ${this.getName()}`); } if (manifestResources.length > 1) { throw new Error(`Found multiple (${manifestResources.length}) manifest.json files ` + `for project ${this.getName()}`); } const resource = manifestResources[0]; try { return { content: JSON.parse(await resource.getString()), filePath: resource.getPath() }; } catch (err) { throw new Error( `Failed to read ${resource.getPath()} for project ${this.getName()}: ${err.message}`); } }); } /** * Reads the .library file * * @returns {Promise<object>} resolves with an object containing the <code>content</code> (as JSON) and * <code>filePath</code> (as string) of the .library file */ async _getDotLibrary() { if (this._pDotLibrary) { return this._pDotLibrary; } return this._pDotLibrary = this._getRawSourceReader().byGlob("**/.library") .then(async (dotLibraryResources) => { if (!dotLibraryResources.length) { throw new Error(`Could not find .library file for project ${this.getName()}`); } if (dotLibraryResources.length > 1) { throw new Error(`Found multiple (${dotLibraryResources.length}) .library files ` + `for project ${this.getName()}`); } const resource = dotLibraryResources[0]; const content = await resource.getString(); try { const { default: xml2js } = await import("xml2js"); const parser = new xml2js.Parser({ explicitArray: false, explicitCharkey: true }); const readXML = promisify(parser.parseString); return { content: await readXML(content), filePath: resource.getPath() }; } catch (err) { throw new Error( `Failed to read ${resource.getPath()} for project ${this.getName()}: ${err.message}`); } }); } /** * Determines the path of the library.js file * * @returns {Promise<string>} resolves with an a string containing the file system path * of the library.js file */ async _getLibraryJsPath() { if (this._pLibraryJs) { return this._pLibraryJs; } return this._pLibraryJs = this._getRawSourceReader().byGlob("**/library.js") .then(async (libraryJsResources) => { if (!libraryJsResources.length) { throw new Error(`Could not find library.js file for project ${this.getName()}`); } if (libraryJsResources.length > 1) { throw new Error(`Found multiple (${libraryJsResources.length}) library.js files ` + `for project ${this.getName()}`); } // Content is not yet relevant, so don't read it return libraryJsResources[0].getPath(); }); } } export default Library;