diff --git a/.gitignore b/.gitignore index 052409d7..a5e3a601 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ build/Release node_modules jspm_packages +# TypeScript cache +*.tsbuildinfo + # Optional npm cache directory .npm @@ -60,3 +63,7 @@ deploy_key # Custom directories test/tmp/ jsdocs/ + +# Generated code +lib/ +!test/lib/ diff --git a/.nycrc.json b/.nycrc.json new file mode 100644 index 00000000..195a297e --- /dev/null +++ b/.nycrc.json @@ -0,0 +1,30 @@ +{ + "extends": "@istanbuljs/nyc-config-typescript", + "reporter": ["lcov", "text", "text-summary"], + "include": ["src/**"], + "check-coverage": true, + "statements": 90, + "branches": 85, + "functions": 90, + "lines": 90, + "watermarks": { + "statements": [ + 70, + 90 + ], + "branches": [ + 70, + 90 + ], + "functions": [ + 70, + 90 + ], + "lines": [ + 70, + 90 + ] + }, + "cache": true, + "all": true +} \ No newline at end of file diff --git a/ava.config.js b/ava.config.js new file mode 100644 index 00000000..a256173d --- /dev/null +++ b/ava.config.js @@ -0,0 +1,21 @@ +// Calculate nodeArguments based on the Node version +const nodeArguments = [ + "--import=tsx/esm", + "--no-warnings=ExperimentalWarning", +]; + +export default { + extensions: { + ts: "module", + }, + files: [ + "test/lib/**/*.ts", + ], + watchMode: { + ignoreChanges: [ + "test/tmp/**", + ], + }, + nodeArguments, + workerThreads: false, +}; diff --git a/eslint.common.config.js b/eslint.common.config.js index 07876d52..f6284f68 100644 --- a/eslint.common.config.js +++ b/eslint.common.config.js @@ -1,99 +1,147 @@ -import jsdoc from "eslint-plugin-jsdoc"; +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import stylistic from "@stylistic/eslint-plugin"; import ava from "eslint-plugin-ava"; -import globals from "globals"; -import js from "@eslint/js"; -import google from "eslint-config-google"; - -export default [{ - ignores: [ // Common ignore patterns across all tooling repos - "**/coverage/", - "test/tmp/", - "test/expected/", - "test/fixtures/", - "**/docs/", - "**/jsdocs/", - ], -}, js.configs.recommended, google, ava.configs["flat/recommended"], { - name: "Common ESLint config used for all tooling repos", +import jsdoc from "eslint-plugin-jsdoc"; - plugins: { - jsdoc, - }, +export default tseslint.config( + { + // This block defines ignore patterns globally to all configurations below + // (therefore it can use slightly different patterns, see also the eslint "Flat Config" doc) + ignores: [ + ".github/*", + ".reuse/*", + "coverage/*", + "**/docs/", + "**/jsdocs/", - languageOptions: { - globals: { - ...globals.node, - }, + // Exclude test files + "test/tmp/*", + "test/fixtures/*", + "test/expected/*", - ecmaVersion: 2023, - sourceType: "module", + // Exclude generated code + "lib/*", + ], }, - - settings: { - jsdoc: { - mode: "jsdoc", - - tagNamePreference: { - return: "returns", - augments: "extends", + // Base configs applying to JS and TS files + eslint.configs.recommended, + stylistic.configs.customize({ + indent: "tab", + quotes: "double", + semi: true, + jsx: false, + arrowParens: true, + braceStyle: "1tbs", + blockSpacing: false, + }), + ava.configs["flat/recommended"], { + // Lint all JS files using the eslint parser + files: ["**/*.js"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + }, + }, { + // Lint all TS files using the typescript-eslint parser + // Also enable all recommended typescript-eslint rules + files: ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts"], + extends: [ + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + parser: tseslint.parser, + parserOptions: { + project: true, }, }, - }, - - rules: { - "indent": ["error", "tab"], - "linebreak-style": ["error", "unix"], - - "quotes": ["error", "double", { - allowTemplateLiterals: true, - }], - - "semi": ["error", "always"], - "no-negated-condition": "off", - "require-jsdoc": "off", - "no-mixed-requires": "off", - - "max-len": ["error", { - code: 120, - ignoreUrls: true, - ignoreRegExpLiterals: true, - }], - - "no-implicit-coercion": [2, { - allow: ["!!"], - }], - - "comma-dangle": "off", - "no-tabs": "off", - "no-console": 2, // Disallow console.log() - "no-eval": 2, - // The following rule must be disabled as of ESLint 9. - // It's removed and causes issues when present - // https://eslint.org/docs/latest/rules/valid-jsdoc - "valid-jsdoc": 0, - "jsdoc/check-examples": 0, - "jsdoc/check-param-names": 2, - "jsdoc/check-tag-names": 2, - "jsdoc/check-types": 2, - "jsdoc/no-undefined-types": 0, - "jsdoc/require-description": 0, - "jsdoc/require-description-complete-sentence": 0, - "jsdoc/require-example": 0, - "jsdoc/require-hyphen-before-param-description": 0, - "jsdoc/require-param": 2, - "jsdoc/require-param-description": 0, - "jsdoc/require-param-name": 2, - "jsdoc/require-param-type": 2, - "jsdoc/require-returns": 0, - "jsdoc/require-returns-description": 0, - "jsdoc/require-returns-type": 2, - - "jsdoc/tag-lines": [2, "any", { - startLines: 1, - }], + rules: { + // TypeScript specific overwrites + // We must disable the base rule as it can report incorrect errors + "no-unused-vars": "off", + "@typescript-eslint/consistent-type-imports": ["error", { + fixStyle: "inline-type-imports", + }], + "@typescript-eslint/consistent-type-exports": ["error", { + fixMixedExportsWithInlineTypeSpecifier: true, + }], + "@typescript-eslint/no-unused-vars": [ + "error", { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, { + // To be discussed: Type-aware checks might add quite some additional work when writing tests + // and could even require us to export types that we would otherwise not export + files: ["test/**/*.ts"], + rules: { + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-enum-comparison": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-unary-minus": "off", + }, + }, { + // Overwrite any rules from the configurations above for both, JS and TS files + rules: { + "linebreak-style": [ + "error", + "unix", + ], + "@stylistic/object-curly-spacing": [ + "error", + "never", + ], + "@stylistic/operator-linebreak": ["error", "after"], + "@stylistic/comma-dangle": ["error", { + functions: "never", + arrays: "always-multiline", + objects: "always-multiline", + imports: "always-multiline", + exports: "always-multiline", + enums: "always-multiline", + generics: "always-multiline", + tuples: "always-multiline", + }], + "max-len": [ + "error", + { + code: 120, + ignoreUrls: true, + ignoreRegExpLiterals: true, + }, + ], + "no-implicit-coercion": [ + "error", + {allow: ["!!"]}, + ], + "no-console": "error", + "no-eval": "error", - "jsdoc/valid-types": 0, - "ava/assertion-arguments": 0, - }, -} -]; + "valid-jsdoc": "off", + }, + }, { + // JSdoc only applying to sources + files: ["src/**/*.ts"], + ...jsdoc.configs["flat/recommended-typescript-error"], + }, { + // Overwriting JSDoc rules in a separate config with the same files pattern + files: ["src/**/*.ts"], + rules: { + "jsdoc/require-jsdoc": "off", + "jsdoc/require-returns": "off", + "jsdoc/require-returns-description": "off", + "jsdoc/tag-lines": ["error", "any", { + startLines: 1, + }], + }, + } +); diff --git a/eslint.config.js b/eslint.config.js index f5873aa9..e87b98da 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,4 +2,8 @@ import eslintCommonConfig from "./eslint.common.config.js"; export default [ ...eslintCommonConfig, // Load common ESLint config + { + // Add project-specific ESLint config rules here + // in order to override common config + }, ]; diff --git a/jsdoc-plugin.cjs b/jsdoc-plugin.cjs index cd7ef446..4305acfe 100644 --- a/jsdoc-plugin.cjs +++ b/jsdoc-plugin.cjs @@ -3,7 +3,7 @@ * JSDoc doesn't see "{@" as a valid type expression, probably as there's also {@link ...}. */ exports.handlers = { - jsdocCommentFound: function(e) { + jsdocCommentFound: function (e) { e.comment = e.comment.replace(/{@ui5\//g, "{ @ui5/"); - } + }, }; diff --git a/lib/AbstractReader.js b/lib/AbstractReader.js deleted file mode 100644 index 8f5c3632..00000000 --- a/lib/AbstractReader.js +++ /dev/null @@ -1,129 +0,0 @@ -import randomInt from "random-int"; -import Trace from "./tracing/Trace.js"; - -/** - * Abstract resource locator implementing the general API for reading resources - * - * @abstract - * @public - * @class - * @alias @ui5/fs/AbstractReader - */ -class AbstractReader { - /** - * The constructor. - * - * @public - * @param {string} name Name of the reader. Typically used for tracing purposes - */ - constructor(name) { - if (new.target === AbstractReader) { - throw new TypeError("Class 'AbstractReader' is abstract"); - } - this._name = name; - } - - /* - * Returns the name of the reader instance. This can be used for logging/tracing purposes. - * - * @returns {string} Name of the reader - */ - getName() { - return this._name || ``; - } - - /** - * Locates resources by matching glob patterns. - * - * @example - * byGlob("**‏/*.{html,htm}"); - * byGlob("**‏/.library"); - * byGlob("/pony/*"); - * - * @public - * @param {string|string[]} virPattern glob pattern as string or array of glob patterns for - * virtual directory structure - * @param {object} [options] glob options - * @param {boolean} [options.nodir=true] Do not match directories - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources - */ - byGlob(virPattern, options = {nodir: true}) { - const trace = new Trace(virPattern); - return this._byGlob(virPattern, options, trace).then(function(result) { - trace.printReport(); - return result; - }).then((resources) => { - if (resources.length > 1) { - // Pseudo randomize result order to prevent consumers from relying on it: - // Swap the first object with a randomly chosen one - const x = 0; - const y = randomInt(0, resources.length - 1); - // Swap object at index "x" with "y" - resources[x] = [resources[y], resources[y]=resources[x]][0]; - } - return resources; - }); - } - - /** - * Locates resources by matching a given path. - * - * @public - * @param {string} virPath Virtual path - * @param {object} [options] Options - * @param {boolean} [options.nodir=true] Do not match directories - * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource - */ - byPath(virPath, options = {nodir: true}) { - const trace = new Trace(virPath); - return this._byPath(virPath, options, trace).then(function(resource) { - trace.printReport(); - return resource; - }); - } - - /** - * Locates resources by one or more glob patterns. - * - * @abstract - * @protected - * @param {string|string[]} virPattern glob pattern as string or an array of - * glob patterns for virtual directory structure - * @param {object} options glob options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources - */ - _byGlob(virPattern, options, trace) { - throw new Error("Function '_byGlob' is not implemented"); - } - - /** - * Locate resources by matching a single glob pattern. - * - * @abstract - * @protected - * @param {string} pattern glob pattern - * @param {object} options glob options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources - */ - _runGlob(pattern, options, trace) { - throw new Error("Function '_runGlob' is not implemented"); - } - - /** - * Locates resources by path. - * - * @abstract - * @protected - * @param {string} virPath Virtual path - * @param {object} options Options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource - */ - _byPath(virPath, options, trace) { - throw new Error("Function '_byPath' is not implemented"); - } -} - -export default AbstractReader; diff --git a/lib/AbstractReaderWriter.js b/lib/AbstractReaderWriter.js deleted file mode 100644 index 9e07da5e..00000000 --- a/lib/AbstractReaderWriter.js +++ /dev/null @@ -1,71 +0,0 @@ -import AbstractReader from "./AbstractReader.js"; - -/** - * Abstract resource locator implementing the general API for reading and writing resources - * - * @abstract - * @public - * @class - * @alias @ui5/fs/AbstractReaderWriter - * @extends @ui5/fs/AbstractReader - */ -class AbstractReaderWriter extends AbstractReader { - /** - * The constructor. - * - * @public - * @param {string} name Name of the reader/writer. Typically used for tracing purposes - */ - constructor(name) { - if (new.target === AbstractReaderWriter) { - throw new TypeError("Class 'AbstractReaderWriter' is abstract"); - } - super(name); - } - - /* - * Returns the name of the reader/writer instance. This can be used for logging/tracing purposes. - * - * @returns {string} Name of the reader/writer - */ - getName() { - return this._name || ``; - } - - /** - * Writes the content of a resource to a path. - * - * @public - * @param {@ui5/fs/Resource} resource Resource to write - * @param {object} [options] - * @param {boolean} [options.readOnly=false] Whether the resource content shall be written read-only - * Do not use in conjunction with the drain option. - * The written file will be used as the new source of this resources content. - * Therefore the written file should not be altered by any means. - * Activating this option might improve overall memory consumption. - * @param {boolean} [options.drain=false] Whether the resource content shall be emptied during the write process. - * Do not use in conjunction with the readOnly option. - * Activating this option might improve overall memory consumption. - * This should be used in cases where this is the last access to the resource. - * E.g. the final write of a resource after all processing is finished. - * @returns {Promise} Promise resolving once data has been written - */ - write(resource, options = {drain: false, readOnly: false}) { - return this._write(resource, options); - } - - /** - * Writes the content of a resource to a path. - * - * @abstract - * @protected - * @param {@ui5/fs/Resource} resource Resource to write - * @param {object} [options] Write options, see above - * @returns {Promise} Promise resolving once data has been written - */ - _write(resource, options) { - throw new Error("Not implemented"); - } -} - -export default AbstractReaderWriter; diff --git a/lib/ReaderCollection.js b/lib/ReaderCollection.js deleted file mode 100644 index f0fa9ffc..00000000 --- a/lib/ReaderCollection.js +++ /dev/null @@ -1,86 +0,0 @@ -import AbstractReader from "./AbstractReader.js"; - -/** - * Resource Locator ReaderCollection - * - * @public - * @class - * @alias @ui5/fs/ReaderCollection - * @extends @ui5/fs/AbstractReader - */ -class ReaderCollection extends AbstractReader { - /** - * The constructor. - * - * @param {object} parameters Parameters - * @param {string} parameters.name The collection name - * @param {@ui5/fs/AbstractReader[]} [parameters.readers] - * List of resource readers (all tried in parallel). - * If none are provided, the collection will never return any results. - */ - constructor({name, readers}) { - super(name); - - // Remove any undefined (empty) readers from array - this._readers = readers.filter(($) => $); - } - - /** - * Locates resources by glob. - * - * @private - * @param {string|string[]} pattern glob pattern as string or an array of - * glob patterns for virtual directory structure - * @param {object} options glob options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources - */ - _byGlob(pattern, options, trace) { - return Promise.all(this._readers.map(function(resourceLocator) { - return resourceLocator._byGlob(pattern, options, trace); - })).then((result) => { - trace.collection(this._name); - return Array.prototype.concat.apply([], result); - }); - } - - /** - * Locates resources by path. - * - * @private - * @param {string} virPath Virtual path - * @param {object} options Options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource|null>} - * Promise resolving to a single resource or null if no resource is found - */ - _byPath(virPath, options, trace) { - const that = this; - const resourceLocatorCount = this._readers.length; - let resolveCount = 0; - - if (resourceLocatorCount === 0) { - // Short-circuit if there are no readers (Promise.race does not settle for empty arrays) - trace.collection(that._name); - return Promise.resolve(null); - } - - // Using Promise.race to deliver files that can be found as fast as possible - return Promise.race(this._readers.map(function(resourceLocator) { - return resourceLocator._byPath(virPath, options, trace).then(function(resource) { - return new Promise(function(resolve, reject) { - trace.collection(that._name); - resolveCount++; - if (resource) { - resource.pushCollection(that._name); - resolve(resource); - } else if (resolveCount === resourceLocatorCount) { - resolve(null); - } - }); - }); - })); - } -} - -export default ReaderCollection; diff --git a/lib/ReaderCollectionPrioritized.js b/lib/ReaderCollectionPrioritized.js deleted file mode 100644 index 680b7135..00000000 --- a/lib/ReaderCollectionPrioritized.js +++ /dev/null @@ -1,90 +0,0 @@ -import AbstractReader from "./AbstractReader.js"; - -/** - * Prioritized Resource Locator Collection - * - * @public - * @class - * @alias @ui5/fs/ReaderCollectionPrioritized - * @extends @ui5/fs/AbstractReader - */ -class ReaderCollectionPrioritized extends AbstractReader { - /** - * The constructor. - * - * @param {object} parameters - * @param {string} parameters.name The collection name - * @param {@ui5/fs/AbstractReader[]} [parameters.readers] - * Prioritized list of resource readers (tried in the order provided). - * If none are provided, the collection will never return any results. - */ - constructor({readers, name}) { - super(name); - - // Remove any undefined (empty) readers from array - this._readers = readers.filter(($) => $); - } - - /** - * Locates resources by glob. - * - * @private - * @param {string|string[]} pattern glob pattern as string or an array of - * glob patterns for virtual directory structure - * @param {object} options glob options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources - */ - _byGlob(pattern, options, trace) { - return Promise.all(this._readers.map(function(resourceLocator) { - return resourceLocator._byGlob(pattern, options, trace); - })).then((result) => { - const files = Object.create(null); - const resources = []; - // Prefer files found in preceding resource locators - for (let i = 0; i < result.length; i++) { - for (let j = 0; j < result[i].length; j++) { - const resource = result[i][j]; - const path = resource.getPath(); - if (!files[path]) { - files[path] = true; - resources.push(resource); - } - } - } - - trace.collection(this._name); - return resources; - }); - } - - /** - * Locates resources by path. - * - * @private - * @param {string} virPath Virtual path - * @param {object} options Options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource|null>} - * Promise resolving to a single resource or null if no resource is found - */ - _byPath(virPath, options, trace) { - const that = this; - const byPath = (i) => { - if (i > this._readers.length - 1) { - return null; - } - return this._readers[i]._byPath(virPath, options, trace).then((resource) => { - if (resource) { - resource.pushCollection(that._name); - return resource; - } else { - return byPath(++i); - } - }); - }; - return byPath(0); - } -} - -export default ReaderCollectionPrioritized; diff --git a/lib/fsInterface.js b/lib/fsInterface.js deleted file mode 100644 index 99d97712..00000000 --- a/lib/fsInterface.js +++ /dev/null @@ -1,92 +0,0 @@ -function toPosix(inputPath) { - return inputPath.replace(/\\/g, "/"); -} - -/** - * @public - * @module @ui5/fs/fsInterface - */ - -/** - * Wraps readers to access them through a [Node.js fs]{@link https://nodejs.org/api/fs.html} styled interface. - * - * @public - * @function default - * @static - * @param {@ui5/fs/AbstractReader} reader Resource Reader or Collection - * - * @returns {object} Object with [Node.js fs]{@link https://nodejs.org/api/fs.html} styled functions - * [readFile]{@link https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback}, - * [stat]{@link https://nodejs.org/api/fs.html#fs_fs_stat_path_options_callback}, - * [readdir]{@link https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback} and - * [mkdir]{@link https://nodejs.org/api/fs.html#fs_fs_mkdir_path_options_callback} - */ -function fsInterface(reader) { - return { - readFile(fsPath, options, callback) { - if (typeof options === "function") { - callback = options; - options = undefined; - } - if (typeof options === "string") { - options = {encoding: options}; - } - const posixPath = toPosix(fsPath); - reader.byPath(posixPath, { - nodir: false - }).then(function(resource) { - if (!resource) { - const error = new Error(`ENOENT: no such file or directory, open '${fsPath}'`); - error.code = "ENOENT"; // "File or directory does not exist" - callback(error); - return; - } - - return resource.getBuffer().then(function(buffer) { - let res; - - if (options && options.encoding) { - res = buffer.toString(options.encoding); - } else { - res = buffer; - } - - callback(null, res); - }); - }).catch(callback); - }, - stat(fsPath, callback) { - const posixPath = toPosix(fsPath); - reader.byPath(posixPath, { - nodir: false - }).then(function(resource) { - if (!resource) { - const error = new Error(`ENOENT: no such file or directory, stat '${fsPath}'`); - error.code = "ENOENT"; // "File or directory does not exist" - callback(error); - } else { - callback(null, resource.getStatInfo()); - } - }).catch(callback); - }, - readdir(fsPath, callback) { - let posixPath = toPosix(fsPath); - if (!posixPath.match(/\/$/)) { - // Add trailing slash if not present - posixPath += "/"; - } - reader.byGlob(posixPath + "*", { - nodir: false - }).then((resources) => { - const files = resources.map((resource) => { - return resource.getName(); - }); - callback(null, files); - }).catch(callback); - }, - mkdir(fsPath, callback) { - setTimeout(callback, 0); - } - }; -} -export default fsInterface; diff --git a/lib/readers/Filter.js b/lib/readers/Filter.js deleted file mode 100644 index b95654da..00000000 --- a/lib/readers/Filter.js +++ /dev/null @@ -1,75 +0,0 @@ -import AbstractReader from "../AbstractReader.js"; - -/** - * A reader that allows dynamic filtering of resources passed through it - * - * @public - * @class - * @alias @ui5/fs/readers/Filter - * @extends @ui5/fs/AbstractReader - */ -class Filter extends AbstractReader { - /** - * Filter callback - * - * @public - * @callback @ui5/fs/readers/Filter~callback - * @param {@ui5/fs/Resource} resource Resource to test - * @returns {boolean} Whether to keep the resource - */ - - /** - * Constructor - * - * @public - * @param {object} parameters Parameters - * @param {@ui5/fs/AbstractReader} parameters.reader The resource reader or collection to wrap - * @param {@ui5/fs/readers/Filter~callback} parameters.callback - * Filter function. Will be called for every resource read through this reader. - */ - constructor({reader, callback}) { - super(); - if (!reader) { - throw new Error(`Missing parameter "reader"`); - } - if (!callback) { - throw new Error(`Missing parameter "callback"`); - } - this._reader = reader; - this._callback = callback; - } - - /** - * Locates resources by glob. - * - * @private - * @param {string|string[]} pattern glob pattern as string or an array of - * glob patterns for virtual directory structure - * @param {object} options glob options - * @param {@ui5/fs/tracing/Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources - */ - async _byGlob(pattern, options, trace) { - const result = await this._reader._byGlob(pattern, options, trace); - return result.filter(this._callback); - } - - /** - * Locates resources by path. - * - * @private - * @param {string} virPath Virtual path - * @param {object} options Options - * @param {@ui5/fs/tracing/Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource - */ - async _byPath(virPath, options, trace) { - const result = await this._reader._byPath(virPath, options, trace); - if (result && !this._callback(result)) { - return null; - } - return result; - } -} - -export default Filter; diff --git a/package-lock.json b/package-lock.json index 7ee84c93..6928a1ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,15 +20,21 @@ "random-int": "^3.0.0" }, "devDependencies": { - "@eslint/js": "^9.8.0", + "@eslint/js": "^9.9.1", "@istanbuljs/esm-loader-hook": "^0.2.0", + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@stylistic/eslint-plugin": "^2.6.4", + "@types/clone": "^2.1.4", + "@types/micromatch": "^4.0.9", + "@types/node": "^20.11.0", + "@types/pretty-hrtime": "^1.0.3", + "@types/sinon": "^17.0.3", "ava": "^6.1.3", "chokidar-cli": "^3.0.0", "cross-env": "^7.0.3", "depcheck": "^1.4.7", "docdash": "^2.0.2", - "eslint": "^9.9.1", - "eslint-config-google": "^0.14.0", + "eslint": "^9.9.0", "eslint-plugin-ava": "^15.0.1", "eslint-plugin-jsdoc": "^50.2.2", "esmock": "^2.6.7", @@ -38,7 +44,11 @@ "open-cli": "^8.0.0", "rimraf": "^6.0.1", "sinon": "^18.0.0", - "tap-xunit": "^2.4.1" + "tap-xunit": "^2.4.1", + "tsx": "^4.17.0", + "typedoc": "^0.26.6", + "typedoc-plugin-rename-defaults": "^0.7.1", + "typescript-eslint": "^8.2.0" }, "engines": { "node": "^20.11.0 || >=22.0.0", @@ -333,6 +343,414 @@ "node": ">=16" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -613,6 +1031,22 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/nyc-config-typescript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", + "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "nyc": ">=15" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -797,113 +1231,559 @@ "node": ">= 8.0.0" } }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@shikijs/core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.14.1.tgz", + "integrity": "sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.6.4.tgz", + "integrity": "sha512-euUGnjzH8EOqEYTGk9dB2OBINp0FX1nuO7/k4fO82zNRBIKZgJoDwTLM4Ce+Om6W1Qmh1PrZjCr4jh4tMEXGPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stylistic/eslint-plugin-js": "2.6.4", + "@stylistic/eslint-plugin-jsx": "2.6.4", + "@stylistic/eslint-plugin-plus": "2.6.4", + "@stylistic/eslint-plugin-ts": "2.6.4", + "@types/eslint": "^9.6.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-js": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.6.4.tgz", + "integrity": "sha512-kx1hS3xTvzxZLdr/DCU/dLBE++vcP97sHeEFX2QXhk1Ipa4K1rzPOLw1HCbf4mU3s+7kHP5eYpDe+QteEOFLug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "^9.6.0", + "acorn": "^8.12.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-jsx": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-2.6.4.tgz", + "integrity": "sha512-bIvVhdtjmyu3S10V7QRIuawtCZSq9gRmzAX23ucjCOdSFzEwlq+di0IM0riBAvvQerrJL4SM6G3xgyPs8BSXIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stylistic/eslint-plugin-js": "^2.6.4", + "@types/eslint": "^9.6.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-jsx/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@stylistic/eslint-plugin-plus": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-2.6.4.tgz", + "integrity": "sha512-EuRvtxhf7Hv8OoMIePulP/6rBJIgPTu1l5GAm1780WcF1Cl8bOZXIn84Pdac5pNv6lVlzCOFm8MD3VE+2YROuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "^9.6.0" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.6.4.tgz", + "integrity": "sha512-yxL8Hj6WkObw1jfiLpBzKy5yfxY6vwlwO4miq34ySErUjUecPV5jxfVbOe4q1QDPKemQGPq93v7sAQS5PzM8lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stylistic/eslint-plugin-js": "2.6.4", + "@types/eslint": "^9.6.0", + "@typescript-eslint/utils": "^8.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, + "node_modules/@types/braces": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.4.tgz", + "integrity": "sha512-0WR3b8eaISjEW7RpZnclONaLFDf7buaowRHdqLp4vLj54AsSAYWfh3DRbfiYJY9XDxMgx1B4sE1Afw2PGpuHOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/clone": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/clone/-/clone-2.1.4.tgz", + "integrity": "sha512-NKRWaEGaVGVLnGLB2GazvDaZnyweW9FJLLFL5LhywGJB3aqGMT9R/EUoJoSRP4nzofYnZysuDmrEJtJdAqUOtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/micromatch": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz", + "integrity": "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/braces": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.16.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.2.tgz", + "integrity": "sha512-91s/n4qUPV/wg8eE9KHYW1kouTfDk2FPGjXbBMfRWP/2vg1rCXNQL1OCabwGs0XSdukuK+MwCDXE30QpSeMUhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, + "node_modules/@types/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", + "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/type-utils": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", + "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", + "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "type-detect": "4.0.8" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "11.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", - "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.1" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "node_modules/@typescript-eslint/utils": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", + "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" } }, - "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, + "license": "MIT", "dependencies": { - "type-detect": "4.0.8" + "@typescript-eslint/types": "8.3.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", - "dev": true - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "dev": true - }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true - }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true - }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true - }, "node_modules/@ui5/logger": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@ui5/logger/-/logger-4.0.1.tgz", @@ -2521,6 +3401,46 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -2600,18 +3520,6 @@ } } }, - "node_modules/eslint-config-google": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", - "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "eslint": ">=5.16.0" - } - }, "node_modules/eslint-plugin-ava": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-ava/-/eslint-plugin-ava-15.0.1.tgz", @@ -3456,6 +4364,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz", + "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3599,6 +4520,13 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4436,6 +5364,13 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -5816,6 +6751,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6027,6 +6972,17 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.14.1.tgz", + "integrity": "sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.14.1", + "@types/hast": "^3.0.4" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -6705,12 +7661,45 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true }, + "node_modules/tsx": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.0.tgz", + "integrity": "sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6750,6 +7739,123 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typedoc": { + "version": "0.26.6", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.6.tgz", + "integrity": "sha512-SfEU3SH3wHNaxhFPjaZE2kNl/NFtLNW5c1oHsg7mti7GjmUj1Roq6osBQeMd+F4kL0BoRBBr8gQAuqBlfFu8LA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "shiki": "^1.9.1", + "yaml": "^2.4.5" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x" + } + }, + "node_modules/typedoc-plugin-rename-defaults": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/typedoc-plugin-rename-defaults/-/typedoc-plugin-rename-defaults-0.7.1.tgz", + "integrity": "sha512-hgg4mAy5IumgUmPOnVVGmGywjTGtUCmRJ2jRbseqtXdlUuYKj652ODL9joUWFt5uvNu4Dr/pNILc/qsKGHJw+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^8.0.0" + }, + "peerDependencies": { + "typedoc": ">=0.22.x <0.27.x" + } + }, + "node_modules/typedoc-plugin-rename-defaults/node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typedoc/node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.3.0.tgz", + "integrity": "sha512-EvWjwWLwwKDIJuBjk2I6UkV8KEQcwZ0VM10nR1rIunRDIP67QJTZAHBXTX0HW/oI1H10YESF8yWie8fRQxjvFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.3.0", + "@typescript-eslint/parser": "8.3.0", + "@typescript-eslint/utils": "8.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -6762,6 +7868,13 @@ "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", "dev": true }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/unicorn-magic": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", diff --git a/package.json b/package.json index 06998ddd..4b643463 100644 --- a/package.json +++ b/package.json @@ -18,24 +18,28 @@ ], "type": "module", "exports": { - "./adapters/*": "./lib/adapters/*.js", - "./AbstractReader": "./lib/AbstractReader.js", - "./AbstractReaderWriter": "./lib/AbstractReaderWriter.js", - "./DuplexCollection": "./lib/DuplexCollection.js", - "./fsInterface": "./lib/fsInterface.js", - "./readers/*": "./lib/readers/*.js", - "./ReaderCollection": "./lib/ReaderCollection.js", - "./ReaderCollectionPrioritized": "./lib/ReaderCollectionPrioritized.js", - "./Resource": "./lib/Resource.js", - "./resourceFactory": "./lib/resourceFactory.js", + "./adapters/*": "./src/adapters/*.ts", + "./AbstractReader": "./src/AbstractReader.ts", + "./AbstractReaderWriter": "./src/AbstractReaderWriter.ts", + "./DuplexCollection": "./src/DuplexCollection.ts", + "./fsInterface": "./src/fsInterface.ts", + "./readers/*": "./src/readers/*.ts", + "./ReaderCollection": "./src/ReaderCollection.ts", + "./ReaderCollectionPrioritized": "./src/ReaderCollectionPrioritized.ts", + "./Resource": "./src/Resource.ts", + "./resourceFactory": "./src/resourceFactory.ts", "./package.json": "./package.json", - "./internal/ResourceTagCollection": "./lib/ResourceTagCollection.js" + "./internal/ResourceTagCollection": "./src/ResourceTagCollection.ts" }, "engines": { "node": "^20.11.0 || >=22.0.0", "npm": ">= 8" }, "scripts": { + "build": "npm run cleanup && tsc -p tsconfig.build.json", + "build-test": "tsc --noEmit -p .", + "build-watch": "npm run cleanup && tsc -w -p tsconfig.build.json", + "cleanup": "rimraf lib coverage", "test": "npm run lint && npm run jsdoc-generate && npm run coverage && npm run depcheck", "test-azure": "npm run coverage-xunit", "lint": "eslint ./", @@ -43,17 +47,18 @@ "unit-verbose": "rimraf test/tmp && cross-env UI5_LOG_LVL=verbose ava --verbose --serial", "unit-watch": "npm run unit -- --watch", "unit-xunit": "rimraf test/tmp && ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\" --tap | tap-xunit --dontUseCommentsAsTestNames=true > test-results.xml", - "unit-inspect": "cross-env UI5_LOG_LVL=verbose ava debug --break", + "unit-debug": "cross-env UI5_LOG_LVL=verbose ava debug", + "unit-update-snapshots": "ava --update-snapshots", "coverage": "rimraf test/tmp && nyc ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\"", "coverage-xunit": "nyc --reporter=text --reporter=text-summary --reporter=cobertura npm run unit-xunit", "jsdoc": "npm run jsdoc-generate && open-cli jsdocs/index.html", - "jsdoc-generate": "jsdoc -c ./jsdoc.json -t $(node -p 'path.dirname(require.resolve(\"docdash\"))') ./lib/ || (echo 'Error during JSDoc generation! Check log.' && exit 1)", - "jsdoc-watch": "npm run jsdoc && chokidar \"./lib/**/*.js\" -c \"npm run jsdoc-generate\"", + "jsdoc-generate": "typedoc", + "jsdoc-watch": "typedoc --watch", "preversion": "npm test", "version": "git-chglog --sort semver --next-tag v$npm_package_version -o CHANGELOG.md v4.0.0.. && git add CHANGELOG.md", "prepublishOnly": "git push --follow-tags", "release-note": "git-chglog --sort semver -c .chglog/release-config.yml v$npm_package_version", - "depcheck": "depcheck --ignores @ui5/fs,docdash,@istanbuljs/esm-loader-hook" + "depcheck": "depcheck --ignores @ui5/fs,@ui5/project,docdash,@istanbuljs/esm-loader-hook,tsx,typedoc-plugin-rename-defaults" }, "files": [ "CHANGELOG.md", @@ -63,21 +68,6 @@ "LICENSES/**", ".reuse/**" ], - "ava": { - "files": [ - "test/lib/**/*.js" - ], - "watchMode": { - "ignoreChanges": [ - "test/tmp/**" - ] - }, - "nodeArguments": [ - "--loader=esmock", - "--no-warnings" - ], - "workerThreads": false - }, "nyc": { "reporter": [ "lcov", @@ -134,24 +124,31 @@ "random-int": "^3.0.0" }, "devDependencies": { - "@eslint/js": "^9.8.0", + "@eslint/js": "^9.9.1", "@istanbuljs/esm-loader-hook": "^0.2.0", + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@stylistic/eslint-plugin": "^2.6.4", + "@types/clone": "^2.1.4", + "@types/micromatch": "^4.0.9", + "@types/node": "^20.11.0", + "@types/pretty-hrtime": "^1.0.3", + "@types/sinon": "^17.0.3", "ava": "^6.1.3", - "chokidar-cli": "^3.0.0", "cross-env": "^7.0.3", "depcheck": "^1.4.7", "docdash": "^2.0.2", - "eslint": "^9.9.1", - "eslint-config-google": "^0.14.0", + "eslint": "^9.9.0", "eslint-plugin-ava": "^15.0.1", "eslint-plugin-jsdoc": "^50.2.2", "esmock": "^2.6.7", - "globals": "^15.9.0", - "jsdoc": "^4.0.3", "nyc": "^17.0.0", "open-cli": "^8.0.0", "rimraf": "^6.0.1", "sinon": "^18.0.0", - "tap-xunit": "^2.4.1" + "tap-xunit": "^2.4.1", + "tsx": "^4.17.0", + "typescript-eslint": "^8.2.0", + "typedoc": "^0.26.6", + "typedoc-plugin-rename-defaults": "^0.7.1" } } diff --git a/src/AbstractReader.ts b/src/AbstractReader.ts new file mode 100644 index 00000000..a508283a --- /dev/null +++ b/src/AbstractReader.ts @@ -0,0 +1,121 @@ +import randomInt from "random-int"; +import Trace from "./tracing/Trace.js"; +import {type ResourceInterface} from "./Resource.js"; + +/** + * Abstract resource locator implementing the general API for reading resources + */ +class AbstractReader { + _name: string | undefined; + /** + * The constructor. + * + * @param name Name of the reader. Typically used for tracing purposes + */ + constructor(name?: string) { + if (new.target === AbstractReader) { + throw new TypeError("Class 'AbstractReader' is abstract"); + } + this._name = name; + } + + /* + * Returns the name of the reader instance. This can be used for logging/tracing purposes. + * + * @returns {string} Name of the reader + */ + getName(): string { + return this._name ?? ``; + } + + /** + * Locates resources by matching glob patterns. + * + * @example + * byGlob("**‏/*.{html,htm}"); + * byGlob("**‏/.library"); + * byGlob("/pony/*"); + * + * @param virPattern glob pattern as string or array of glob patterns for + * virtual directory structure + * @param [options] glob options + * @param [options.nodir] Do not match directories + * @returns Promise resolving to list of resources + */ + byGlob(virPattern: string | string[], options = {nodir: true}): Promise { + const trace = new Trace(Array.isArray(virPattern) ? virPattern.join("") : virPattern); + return this._byGlob(virPattern, options, trace).then(function (result: ResourceInterface[]) { + trace.printReport(); + return result; + }).then((resources: ResourceInterface[]) => { + if (resources.length > 1) { + // Pseudo randomize result order to prevent consumers from relying on it: + // Swap the first object with a randomly chosen one + const x = 0; + const y = randomInt(0, resources.length - 1); + // Swap object at index "x" with "y" + resources[x] = [resources[y], resources[y] = resources[x]][0]; + } + return resources; + }); + } + + /** + * Locates resources by matching a given path. + * + * @param virPath Virtual path + * @param [options] Options + * @param [options.nodir] Do not match directories + * @returns Promise resolving to a single resource + */ + byPath(virPath: string, options = {nodir: true}) { + const trace = new Trace(virPath); + return this._byPath(virPath, options, trace).then(function (resource) { + trace.printReport(); + return resource; + }); + } + + /** + * Locates resources by one or more glob patterns. + * + * @param _virPattern glob pattern as string or an array of + * glob patterns for virtual directory structure + * @param _options glob options + * @param _options.nodir Do not match directories + * @param _trace Trace instance + */ + _byGlob(_virPattern: string | string[], + _options: { + nodir: boolean; + }, + _trace: Trace): Promise { + throw new Error("Function '_byGlob' is not implemented"); + } + + /** + * Locate resources by matching a single glob pattern. + * + * @param _pattern glob pattern + * @param _options glob options + * @param _options.nodir Do not match directories + * @param _trace Trace instance + */ + _runGlob(_pattern: string | string[], _options: {nodir: boolean}, _trace: Trace): Promise { + throw new Error("Function '_runGlob' is not implemented"); + } + + /** + * Locates resources by path. + * + * @param _virPath Virtual path + * @param _options glob options + * @param _options.nodir Do not match directories + * @param _trace Trace instance + */ + _byPath(_virPath: string, _options: {nodir: boolean}, _trace: Trace): Promise { + throw new Error("Function '_byPath' is not implemented"); + } +} + +export default AbstractReader; diff --git a/src/AbstractReaderWriter.ts b/src/AbstractReaderWriter.ts new file mode 100644 index 00000000..8092b1ec --- /dev/null +++ b/src/AbstractReaderWriter.ts @@ -0,0 +1,71 @@ +import AbstractReader from "./AbstractReader.js"; +import {type ResourceInterface} from "./Resource.js"; + +/** + * Abstract resource locator implementing the general API for reading and writing resources + */ +class AbstractReaderWriter extends AbstractReader { + /** + * The constructor. + * + * @param name Name of the reader/writer. Typically used for tracing purposes + */ + constructor(name?: string) { + if (new.target === AbstractReaderWriter) { + throw new TypeError("Class 'AbstractReaderWriter' is abstract"); + } + super(name); + } + + /* + * Returns the name of the reader/writer instance. This can be used for logging/tracing purposes. + * + * @returns {string} Name of the reader/writer + */ + getName(): string { + return this._name ?? ``; + } + + /** + * Writes the content of a resource to a path. + * + * @param resource Resource to write + * @param [options] Write options + * @param [options.readOnly] Whether the resource content shall be written read-only + * Do not use in conjunction with the drain option. + * The written file will be used as the new source of this resources content. + * Therefore the written file should not be altered by any means. + * Activating this option might improve overall memory consumption. + * @param [options.drain] Whether the resource content shall be emptied during the write process. + * Do not use in conjunction with the readOnly option. + * Activating this option might improve overall memory consumption. + * This should be used in cases where this is the last access to the resource. + * E.g. the final write of a resource after all processing is finished. + * @returns Promise resolving once data has been written + */ + write(resource: ResourceInterface, options = {drain: false, readOnly: false}): Promise { + return this._write(resource, options); + } + + /** + * Writes the content of a resource to a path. + * + * @param _resource Resource to write + * @param [_options] Write options, see above + * @param [_options.drain] Whether the resource content shall be emptied during the write process. + * Do not use in conjunction with the readOnly option. + * Activating this option might improve overall memory consumption. + * This should be used in cases where this is the last access to the resource. + * E.g. the final write of a resource after all processing is finished. + * @param [_options.readOnly] Whether the resource content shall be written read-only + * Do not use in conjunction with the drain option. + * The written file will be used as the new source of this resources content. + * Therefore the written file should not be altered by any means. + * Activating this option might improve overall memory consumption. + */ + _write(_resource: ResourceInterface, _options?: {drain?: boolean; readOnly?: boolean}): Promise { + throw new Error("Not implemented"); + } +} + +export default AbstractReaderWriter; diff --git a/lib/DuplexCollection.js b/src/DuplexCollection.ts similarity index 50% rename from lib/DuplexCollection.js rename to src/DuplexCollection.ts index 6aec0ec7..c29ff93f 100644 --- a/lib/DuplexCollection.js +++ b/src/DuplexCollection.ts @@ -1,25 +1,27 @@ +import type AbstractReader from "./AbstractReader.js"; import AbstractReaderWriter from "./AbstractReaderWriter.js"; import ReaderCollectionPrioritized from "./ReaderCollectionPrioritized.js"; +import {type ResourceInterface} from "./Resource.js"; +import type Trace from "./tracing/Trace.js"; /** * Wrapper to keep readers and writers together - * - * @public - * @class - * @alias @ui5/fs/DuplexCollection - * @extends @ui5/fs/AbstractReaderWriter */ class DuplexCollection extends AbstractReaderWriter { + _reader: AbstractReader; + _writer: AbstractReaderWriter; + _combo: ReaderCollectionPrioritized; + /** * The Constructor. * - * @param {object} parameters - * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers - * @param {@ui5/fs/AbstractReaderWriter} parameters.writer + * @param parameters Parameters + * @param parameters.reader Single reader or collection of readers + * @param parameters.writer * A ReaderWriter instance which is only used for writing files - * @param {string} [parameters.name=""] The collection name + * @param [parameters.name] The collection name */ - constructor({reader, writer, name = ""}) { + constructor({reader, writer, name = ""}: {reader: AbstractReader; writer: AbstractReaderWriter; name?: string}) { super(name); if (!reader) { @@ -36,47 +38,46 @@ class DuplexCollection extends AbstractReaderWriter { name: `${name} - ReaderCollectionPrioritized`, readers: [ writer, - reader - ] + reader, + ], }); } /** * Locates resources by glob. * - * @private - * @param {string|string[]} virPattern glob pattern as string or an array of + * @param virPattern glob pattern as string or an array of * glob patterns for virtual directory structure - * @param {object} options glob options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving with a list of resources + * @param options glob options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving with a list of resources */ - _byGlob(virPattern, options, trace) { + _byGlob(virPattern: string | string[], options: {nodir: boolean}, trace: Trace) { return this._combo._byGlob(virPattern, options, trace); } /** * Locates resources by path. * - * @private - * @param {string} virPath Virtual path - * @param {object} options Options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource|null>} + * @param virPath Virtual path + * @param options Options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns * Promise resolving to a single resource or null if no resource is found */ - _byPath(virPath, options, trace) { + _byPath(virPath: string, options: {nodir: boolean}, trace: Trace) { return this._combo._byPath(virPath, options, trace); } /** * Writes the content of a resource to a path. * - * @private - * @param {@ui5/fs/Resource} resource The Resource to write - * @returns {Promise} Promise resolving once data has been written + * @param resource The Resource to write + * @returns Promise resolving once data has been written */ - _write(resource) { + _write(resource: ResourceInterface) { return this._writer.write(resource); } } diff --git a/src/ReaderCollection.ts b/src/ReaderCollection.ts new file mode 100644 index 00000000..1354f813 --- /dev/null +++ b/src/ReaderCollection.ts @@ -0,0 +1,79 @@ +import AbstractReader from "./AbstractReader.js"; +import {type ResourceInterface} from "./Resource.js"; +import type Trace from "./tracing/Trace.js"; + +/** + * Resource Locator ReaderCollection + */ +class ReaderCollection extends AbstractReader { + _readers: AbstractReader[]; + /** + * The constructor. + * + * @param parameters Parameters + * @param [parameters.name] The collection name + * @param [parameters.readers] + * List of resource readers (all tried in parallel). + * If none are provided, the collection will never return any results. + */ + constructor({name, readers}: {name?: string; readers?: AbstractReader[]}) { + super(name); + + // Remove any undefined (empty) readers from array + this._readers = readers?.filter(($) => $) ?? []; + } + + /** + * Locates resources by glob. + * + * @param pattern glob pattern as string or an array of + * glob patterns for virtual directory structure + * @param options glob options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to list of resources + */ + _byGlob(pattern: string | string[], options: {nodir: boolean}, trace: Trace) { + return Promise.all(this._readers.map(function (resourceLocator) { + return resourceLocator._byGlob(pattern, options, trace); + })).then((result) => { + trace.collection(this._name!); + return Array.prototype.concat.apply([], result) as ResourceInterface[]; // Flatten array + }); + } + + /** + * Locates resources by path. + * + * @param virPath Virtual path + * @param options Options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns + * Promise resolving to a single resource or null if no resource is found + */ + _byPath(virPath: string, options: {nodir: boolean}, trace: Trace) { + const resourceLocatorCount = this._readers.length; + + if (resourceLocatorCount === 0) { + // Short-circuit if there are no readers (Promise.race does not settle for empty arrays) + trace.collection(this._name!); + return Promise.resolve(null); + } + + // Using Promise.race to deliver files that can be found as fast as possible + return Promise.race(this._readers.map((resourceLocator) => { + return resourceLocator._byPath(virPath, options, trace).then((resource) => { + trace.collection(this._name!); + if (resource) { + resource.pushCollection(this._name!); + return resource; + } else { + return null; + } + }); + })); + } +} + +export default ReaderCollection; diff --git a/src/ReaderCollectionPrioritized.ts b/src/ReaderCollectionPrioritized.ts new file mode 100644 index 00000000..4ad4aba6 --- /dev/null +++ b/src/ReaderCollectionPrioritized.ts @@ -0,0 +1,93 @@ +import AbstractReader from "./AbstractReader.js"; +import {type ResourceInterface} from "./Resource.js"; +import type Trace from "./tracing/Trace.js"; + +/** + * Prioritized Resource Locator Collection + */ +class ReaderCollectionPrioritized extends AbstractReader { + _readers: AbstractReader[]; + + /** + * The constructor. + * + * @param parameters Parameters + * @param parameters.name The collection name + * @param [parameters.readers] + * Prioritized list of resource readers (tried in the order provided). + * If none are provided, the collection will never return any results. + */ + constructor({readers, name}: {readers?: AbstractReader[]; name: string}) { + super(name); + + // Remove any undefined (empty) readers from array + this._readers = readers?.filter(($) => $) ?? []; + } + + /** + * Locates resources by glob. + * + * @param pattern glob pattern as string or an array of + * glob patterns for virtual directory structure + * @param options glob options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to list of resources + */ + _byGlob(pattern: string | string[], options: {nodir: boolean}, trace: Trace) { + return Promise.all(this._readers.map(function (resourceLocator) { + return resourceLocator._byGlob(pattern, options, trace); + })).then((result) => { + const files = Object.create(null) as Record; + const resources = []; + // Prefer files found in preceding resource locators + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < result.length; i++) { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let j = 0; j < result[i].length; j++) { + const resource = result[i][j]; + const path = resource.getPath(); + if (!files[path]) { + files[path] = true; + resources.push(resource); + } + } + } + + trace.collection(this._name!); + return resources; + }); + } + + /** + * Locates resources by path. + * + * @param virPath Virtual path + * @param options Options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns + * Promise resolving to a single resource or null if no resource is found + */ + _byPath(virPath: string, options: {nodir: boolean}, trace: Trace) { + // const that = this; + const byPath = (i: number) => { + if (i > this._readers.length - 1) { + return Promise.resolve(null); + } + return this._readers[i]._byPath(virPath, options, trace) + .then((resource: ResourceInterface | null): ResourceInterface | Promise => { + if (resource) { + resource.pushCollection(this._name!); + return resource; + } else { + return byPath(++i); + } + }); + }; + + return byPath(0); + } +} + +export default ReaderCollectionPrioritized; diff --git a/lib/Resource.js b/src/Resource.ts similarity index 63% rename from lib/Resource.js rename to src/Resource.ts index 0eb10b39..3b61aa93 100644 --- a/lib/Resource.js +++ b/src/Resource.ts @@ -1,69 +1,133 @@ import stream from "node:stream"; import clone from "clone"; import posixPath from "node:path/posix"; +import {Buffer} from "node:buffer"; +import type {Stats} from "node:fs"; +import type {Project} from "@ui5/project/specifications/Project"; +import {isString} from "./utils/tsUtils.js"; const fnTrue = () => true; const fnFalse = () => false; -const ALLOWED_SOURCE_METADATA_KEYS = ["adapter", "fsPath", "contentModified"]; + +enum ALLOWED_SOURCE_METADATA_KEYS { + ADAPTER = "adapter", + FS_PATH = "fsPath", + CONTENT_MODIFIED = "contentModified", +}; /** - * Resource. UI5 Tooling specific representation of a file's content and metadata + * Function for dynamic creation of content streams * - * @public - * @class - * @alias @ui5/fs/Resource + * @returns {stream.Readable} A readable stream of a resources content */ -class Resource { - #project; - #buffer; - #buffering; - #collections; - #contentDrained; - #createStream; - #name; - #path; - #sourceMetadata; - #statInfo; - #stream; - #streamDrained; - #isModified; +export type Resource_CreateReadableStream = () => stream.Readable; + +export interface Resource_sourceMetadata { + [ALLOWED_SOURCE_METADATA_KEYS.ADAPTER]?: string; + [ALLOWED_SOURCE_METADATA_KEYS.FS_PATH]?: string; + [ALLOWED_SOURCE_METADATA_KEYS.CONTENT_MODIFIED]?: boolean; +}; + +// TODO: Validate these options. +// Some might be required while others can be optional. +// Different combinations can be ok. +export interface Resource_Options { + path: string; + // It could be a real Stats, but also a Stats like object + statInfo?: Partial; + buffer?: Buffer; + string?: string; + createStream?: Resource_CreateReadableStream; + stream?: stream.Readable; + project?: Project; + sourceMetadata?: Resource_sourceMetadata; + source?: { + adapter?: "Abstract"; + }; +}; + +export interface Tree {[x: string]: object | Tree}; + +export interface LegacyResource { + _path: string; + // It could be a real Stats, but also a Stats like object + _statInfo?: Partial; + _source?: { + adapter?: "Abstract"; + }; + _createStream?: Resource_CreateReadableStream; + _stream?: stream.Readable; + _buffer?: Buffer; + _getBufferFromStream?: () => Promise; +} - /** - * Function for dynamic creation of content streams - * - * @public - * @callback @ui5/fs/Resource~createStream - * @returns {stream.Readable} A readable stream of a resources content - */ +export interface ResourceInterface { + clone(): Promise; + getBuffer(): Promise; + getName(): string; + getPath(): string; + getPathTree(): Tree; + getProject(): Project | undefined; + getSourceMetadata(): Resource_sourceMetadata; + getSize(): Promise; + getStatInfo(): Partial; + getStream(): stream.Readable; + getString(): Promise; + hasProject(): boolean; + isModified(): boolean; + pushCollection(name: string): void; + setBuffer(buffer: Buffer): void; + setPath(path: string): void; + setProject(project: Project): void; + setStream(stream: stream.Readable | Resource_CreateReadableStream): void; + setString(string: string): void; +} +/** + * Resource. UI5 Tooling specific representation of a file's content and metadata + */ +class Resource implements ResourceInterface { + #project; + #buffer: Buffer | null | undefined; + #buffering: Promise | null | undefined; + #collections: string[]; + #contentDrained: boolean | undefined; + #createStream: Resource_CreateReadableStream | null | undefined; + #name!: string; + #path!: string; + #sourceMetadata: Resource_sourceMetadata; + #statInfo: Partial; + #stream: stream.Readable | null | undefined; + #streamDrained: boolean | undefined; + #isModified: boolean; /** * - * @public - * @param {object} parameters Parameters - * @param {string} parameters.path Absolute virtual path of the resource - * @param {fs.Stats|object} [parameters.statInfo] File information. Instance of + * @param parameters Parameters + * @param parameters.path Absolute virtual path of the resource + * @param [parameters.statInfo] File information. Instance of * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} or similar object - * @param {Buffer} [parameters.buffer] Content of this resources as a Buffer instance + * @param [parameters.buffer] Content of this resources as a Buffer instance * (cannot be used in conjunction with parameters string, stream or createStream) - * @param {string} [parameters.string] Content of this resources as a string + * @param [parameters.string] Content of this resources as a string * (cannot be used in conjunction with parameters buffer, stream or createStream) - * @param {Stream} [parameters.stream] Readable stream of the content of this resource + * @param [parameters.stream] Readable stream of the content of this resource * (cannot be used in conjunction with parameters buffer, string or createStream) - * @param {@ui5/fs/Resource~createStream} [parameters.createStream] Function callback that returns a readable + * @param [parameters.createStream] Function callback that returns a readable * stream of the content of this resource (cannot be used in conjunction with parameters buffer, * string or stream). * In some cases this is the most memory-efficient way to supply resource content - * @param {@ui5/project/specifications/Project} [parameters.project] Project this resource is associated with - * @param {object} [parameters.sourceMetadata] Source metadata for UI5 Tooling internal use. + * @param [parameters.project] Project this resource is associated with + * @param [parameters.sourceMetadata] Source metadata for UI5 Tooling internal use. * Some information may be set by an adapter to store information for later retrieval. Also keeps track of whether * a resource content has been modified since it has been read from a source */ - constructor({path, statInfo, buffer, string, createStream, stream, project, sourceMetadata}) { + constructor({path, statInfo, buffer, string, createStream, stream, project, sourceMetadata}: Resource_Options + ) { if (!path) { throw new Error("Unable to create Resource: Missing parameter 'path'"); } - if (buffer && createStream || buffer && string || string && createStream || buffer && stream || - string && stream || createStream && stream) { + if ((buffer && createStream) || (buffer && string) || (string && createStream) || (buffer && stream) || + (string && stream) || (createStream && stream)) { throw new Error("Unable to create Resource: Please set only one content parameter. " + "'buffer', 'string', 'stream' or 'createStream'"); } @@ -73,12 +137,16 @@ class Resource { throw new Error(`Parameter 'sourceMetadata' must be of type "object"`); } - /* eslint-disable-next-line guard-for-in */ + // TODO: TS Those checks are completely redundant, but some tests + // and maybe runtime code would rely on them. A major refactoring + // would be needed for (const metadataKey in sourceMetadata) { // Also check prototype - if (!ALLOWED_SOURCE_METADATA_KEYS.includes(metadataKey)) { + if (!Object.values(ALLOWED_SOURCE_METADATA_KEYS) + .includes(metadataKey as ALLOWED_SOURCE_METADATA_KEYS)) { throw new Error(`Parameter 'sourceMetadata' contains an illegal attribute: ${metadataKey}`); } - if (!["string", "boolean"].includes(typeof sourceMetadata[metadataKey])) { + if (!["string", "boolean"] + .includes(typeof sourceMetadata[metadataKey as ALLOWED_SOURCE_METADATA_KEYS])) { throw new Error( `Attribute '${metadataKey}' of parameter 'sourceMetadata' ` + `must be of type "string" or "boolean"`); @@ -88,7 +156,7 @@ class Resource { this.setPath(path); - this.#sourceMetadata = sourceMetadata || {}; + this.#sourceMetadata = sourceMetadata ?? {}; // This flag indicates whether a resource has changed from its original source. // resource.isModified() is not sufficient, since it only reflects the modification state of the @@ -100,7 +168,7 @@ class Resource { this.#project = project; - this.#statInfo = statInfo || { // TODO + this.#statInfo = statInfo ?? { // TODO isFile: fnTrue, isDirectory: fnFalse, isBlockDevice: fnFalse, @@ -115,7 +183,7 @@ class Resource { atime: new Date(), mtime: new Date(), ctime: new Date(), - birthtime: new Date() + birthtime: new Date(), }; if (createStream) { @@ -125,7 +193,7 @@ class Resource { } else if (buffer) { // Use private setter, not to accidentally set any modified flags this.#setBuffer(buffer); - } else if (typeof string === "string" || string instanceof String) { + } else if (isString(string)) { // Use private setter, not to accidentally set any modified flags this.#setBuffer(Buffer.from(string, "utf8")); } @@ -137,10 +205,9 @@ class Resource { /** * Gets a buffer with the resource content. * - * @public - * @returns {Promise} Promise resolving with a buffer of the resource content. + * @returns Promise resolving with a buffer of the resource content. */ - async getBuffer() { + async getBuffer(): Promise { if (this.#contentDrained) { throw new Error(`Content of Resource ${this.#path} has been drained. ` + "This might be caused by requesting resource content after a content stream has been " + @@ -158,16 +225,15 @@ class Resource { /** * Sets a Buffer as content. * - * @public - * @param {Buffer} buffer Buffer instance + * @param buffer Buffer instance */ - setBuffer(buffer) { + setBuffer(buffer: Buffer) { this.#sourceMetadata.contentModified = true; this.#isModified = true; this.#setBuffer(buffer); } - #setBuffer(buffer) { + #setBuffer(buffer: Buffer) { this.#createStream = null; // if (this.#stream) { // TODO this may cause strange issues // this.#stream.destroy(); @@ -181,10 +247,9 @@ class Resource { /** * Gets a string with the resource content. * - * @public - * @returns {Promise} Promise resolving with the resource content. + * @returns Promise resolving with the resource content. */ - getString() { + getString(): Promise { if (this.#contentDrained) { return Promise.reject(new Error(`Content of Resource ${this.#path} has been drained. ` + "This might be caused by requesting resource content after a content stream has been " + @@ -196,10 +261,9 @@ class Resource { /** * Sets a String as content * - * @public - * @param {string} string Resource content + * @param string Resource content */ - setString(string) { + setString(string: string) { this.setBuffer(Buffer.from(string, "utf8")); } @@ -211,10 +275,9 @@ class Resource { * or [setString]{@link @ui5/fs/Resource#setString}). This * is to prevent consumers from accessing drained streams. * - * @public - * @returns {stream.Readable} Readable stream for the resource content. + * @returns Readable stream for the resource content. */ - getStream() { + getStream(): stream.Readable { if (this.#contentDrained) { throw new Error(`Content of Resource ${this.#path} has been drained. ` + "This might be caused by requesting resource content after a content stream has been " + @@ -246,11 +309,10 @@ class Resource { /** * Sets a readable stream as content. * - * @public - * @param {stream.Readable|@ui5/fs/Resource~createStream} stream Readable stream of the resource content or + * @param stream Readable stream of the resource content or callback for dynamic creation of a readable stream */ - setStream(stream) { + setStream(stream: stream.Readable | Resource_CreateReadableStream) { this.#isModified = true; this.#sourceMetadata.contentModified = true; @@ -272,20 +334,18 @@ class Resource { /** * Gets the virtual resources path * - * @public - * @returns {string} Virtual path of the resource + * @returns Virtual path of the resource */ - getPath() { - return this.#path; + getPath(): string { + return this.#path ?? ""; } /** * Sets the virtual resources path * - * @public - * @param {string} path Absolute virtual path of the resource + * @param path Absolute virtual path of the resource */ - setPath(path) { + setPath(path: string) { path = posixPath.normalize(path); if (!posixPath.isAbsolute(path)) { throw new Error(`Unable to set resource path: Path must be absolute: ${path}`); @@ -297,10 +357,9 @@ class Resource { /** * Gets the resource name * - * @public - * @returns {string} Name of the resource + * @returns Name of the resource */ - getName() { + getName(): string { return this.#name; } @@ -310,11 +369,10 @@ class Resource { * Also, depending on the used adapter, some fields might be missing which would be present for a * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} instance. * - * @public - * @returns {fs.Stats|object} Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} + * @returns Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} * or similar object */ - getStatInfo() { + getStatInfo(): Partial { return this.#statInfo; } @@ -322,9 +380,9 @@ class Resource { * Size in bytes allocated by the underlying buffer. * * @see {TypedArray#byteLength} - * @returns {Promise} size in bytes, 0 if there is no content yet + * @returns size in bytes, 0 if there is no content yet */ - async getSize() { + async getSize(): Promise { // if resource does not have any content it should have 0 bytes if (!this.#buffer && !this.#createStream && !this.#stream) { return 0; @@ -336,28 +394,27 @@ class Resource { /** * Adds a resource collection name that was involved in locating this resource. * - * @param {string} name Resource collection name + * @param name Resource collection name */ - pushCollection(name) { + pushCollection(name: string) { this.#collections.push(name); } /** * Returns a clone of the resource. The clones content is independent from that of the original resource * - * @public - * @returns {Promise<@ui5/fs/Resource>} Promise resolving with the clone + * @returns Promise resolving with the clone */ - async clone() { + async clone(): Promise { const options = await this.#getCloneOptions(); return new Resource(options); } - async #getCloneOptions() { - const options = { + async #getCloneOptions(): Promise { + const options: Resource_Options = { path: this.#path, statInfo: clone(this.#statInfo), - sourceMetadata: clone(this.#sourceMetadata) + sourceMetadata: clone(this.#sourceMetadata), }; if (this.#stream) { @@ -381,23 +438,21 @@ class Resource { * [MiddlewareUtil]{@link module:@ui5/server.middleware.MiddlewareUtil}, which will * return a Specification Version-compatible Project interface. * - * @public - * @returns {@ui5/project/specifications/Project} Project this resource is associated with + * @returns Project this resource is associated with */ - getProject() { + getProject(): Project | undefined { return this.#project; } /** * Assign a project to the resource * - * @public - * @param {@ui5/project/specifications/Project} project Project this resource is associated with + * @param project Project this resource is associated with */ - setProject(project) { + setProject(project: Project) { if (this.#project) { throw new Error(`Unable to assign project ${project.getName()} to resource ${this.#path}: ` + - `Resource is already associated to project ${this.#project}`); + `Resource is already associated to project ${this.#project.getName()}`); } this.#project = project; } @@ -405,35 +460,33 @@ class Resource { /** * Check whether a project has been assigned to the resource * - * @public - * @returns {boolean} True if the resource is associated with a project + * @returns True if the resource is associated with a project */ - hasProject() { + hasProject(): boolean { return !!this.#project; } /** * Check whether the content of this resource has been changed during its life cycle * - * @public - * @returns {boolean} True if the resource's content has been changed + * @returns True if the resource's content has been changed */ - isModified() { + isModified(): boolean { return this.#isModified; } /** * Tracing: Get tree for printing out trace * - * @returns {object} Trace tree + * @returns Trace tree */ - getPathTree() { - const tree = Object.create(null); + getPathTree(): Tree { + const tree = Object.create(null) as Tree; - let pointer = tree[this.#path] = Object.create(null); + let pointer = tree[this.#path] = Object.create(null) as Tree; for (let i = this.#collections.length - 1; i >= 0; i--) { - pointer = pointer[this.#collections[i]] = Object.create(null); + pointer = pointer[this.#collections[i]] = Object.create(null) as Tree; } return tree; @@ -443,19 +496,18 @@ class Resource { * Returns source metadata which may contain information specific to the adapter that created the resource * Typically set by an adapter to store information for later retrieval. * - * @returns {object} + * @returns */ - getSourceMetadata() { + getSourceMetadata(): Resource_sourceMetadata { return this.#sourceMetadata; } /** * Returns the content as stream. * - * @private - * @returns {stream.Readable} Readable stream + * @returns Readable stream */ - #getStream() { + #getStream(): stream.Readable { if (this.#streamDrained) { throw new Error(`Content stream of Resource ${this.#path} is flagged as drained.`); } @@ -463,23 +515,23 @@ class Resource { return this.#createStream(); } this.#streamDrained = true; - return this.#stream; + return this.#stream!; } /** * Converts the buffer into a stream. * - * @private - * @returns {Promise} Promise resolving with buffer. + * @returns Promise resolving with buffer. */ - #getBufferFromStream() { + #getBufferFromStream(): Promise { if (this.#buffering) { // Prevent simultaneous buffering, causing unexpected access to drained stream return this.#buffering; } return this.#buffering = new Promise((resolve, reject) => { const contentStream = this.#getStream(); - const buffers = []; - contentStream.on("data", (data) => { + const buffers: Buffer[] = []; + + contentStream.on("data", (data: Buffer) => { buffers.push(data); }); contentStream.on("error", (err) => { diff --git a/lib/ResourceFacade.js b/src/ResourceFacade.ts similarity index 70% rename from lib/ResourceFacade.js rename to src/ResourceFacade.ts index ef6dcdf8..7a864e65 100644 --- a/lib/ResourceFacade.js +++ b/src/ResourceFacade.ts @@ -1,25 +1,24 @@ import posixPath from "node:path/posix"; +import {type Buffer} from "node:buffer"; +import type stream from "node:stream"; +import {type Resource_CreateReadableStream, type ResourceInterface} from "./Resource.js"; +import {type Project} from "@ui5/project/specifications/Project"; /** * A {@link @ui5/fs/Resource Resource} with a different path than it's original - * - * @public - * @class - * @alias @ui5/fs/ResourceFacade */ -class ResourceFacade { +class ResourceFacade implements ResourceInterface { #path; #name; #resource; /** * - * @public - * @param {object} parameters Parameters - * @param {string} parameters.path Virtual path of the facade resource - * @param {@ui5/fs/Resource} parameters.resource Resource to conceal + * @param parameters Parameters + * @param parameters.path Virtual path of the facade resource + * @param parameters.resource Resource to conceal */ - constructor({path, resource}) { + constructor({path, resource}: {path: string; resource: ResourceInterface}) { if (!path) { throw new Error("Unable to create ResourceFacade: Missing parameter 'path'"); } @@ -38,8 +37,7 @@ class ResourceFacade { /** * Gets the resources path * - * @public - * @returns {string} (Virtual) path of the resource + * @returns (Virtual) path of the resource */ getPath() { return this.#path; @@ -48,8 +46,7 @@ class ResourceFacade { /** * Gets the resource name * - * @public - * @returns {string} Name of the resource + * @returns Name of the resource */ getName() { return this.#name; @@ -58,10 +55,9 @@ class ResourceFacade { /** * Sets the resources path * - * @public - * @param {string} path (Virtual) path of the resource + * @param _path (Virtual) path of the resource */ - setPath(path) { + setPath(_path: string) { throw new Error(`The path of a ResourceFacade can't be changed`); } @@ -69,8 +65,7 @@ class ResourceFacade { * Returns a clone of the resource. The clones content is independent from that of the original resource. * A ResourceFacade becomes a Resource * - * @public - * @returns {Promise<@ui5/fs/Resource>} Promise resolving with the clone + * @returns Promise resolving with the clone */ async clone() { // Cloning resolves the facade @@ -87,8 +82,7 @@ class ResourceFacade { /** * Gets a buffer with the resource content. * - * @public - * @returns {Promise} Promise resolving with a buffer of the resource content. + * @returns Promise resolving with a buffer of the resource content. */ async getBuffer() { return this.#resource.getBuffer(); @@ -97,18 +91,16 @@ class ResourceFacade { /** * Sets a Buffer as content. * - * @public - * @param {Buffer} buffer Buffer instance + * @param buffer Buffer instance */ - setBuffer(buffer) { + setBuffer(buffer: Buffer) { return this.#resource.setBuffer(buffer); } /** * Gets a string with the resource content. * - * @public - * @returns {Promise} Promise resolving with the resource content. + * @returns Promise resolving with the resource content. */ getString() { return this.#resource.getString(); @@ -117,10 +109,9 @@ class ResourceFacade { /** * Sets a String as content * - * @public - * @param {string} string Resource content + * @param string Resource content */ - setString(string) { + setString(string: string) { return this.#resource.setString(string); } @@ -132,8 +123,7 @@ class ResourceFacade { * or [setString]{@link @ui5/fs/Resource#setString}). This * is to prevent consumers from accessing drained streams. * - * @public - * @returns {stream.Readable} Readable stream for the resource content. + * @returns Readable stream for the resource content. */ getStream() { return this.#resource.getStream(); @@ -142,11 +132,10 @@ class ResourceFacade { /** * Sets a readable stream as content. * - * @public - * @param {stream.Readable|@ui5/fs/Resource~createStream} stream Readable stream of the resource content or + * @param stream Readable stream of the resource content or callback for dynamic creation of a readable stream */ - setStream(stream) { + setStream(stream: stream.Readable | Resource_CreateReadableStream) { return this.#resource.setStream(stream); } @@ -156,8 +145,7 @@ class ResourceFacade { * Also, depending on the used adapter, some fields might be missing which would be present for a * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} instance. * - * @public - * @returns {fs.Stats|object} Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} + * @returns Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} * or similar object */ getStatInfo() { @@ -168,25 +156,25 @@ class ResourceFacade { * Size in bytes allocated by the underlying buffer. * * @see {TypedArray#byteLength} - * @returns {Promise} size in bytes, 0 if there is no content yet + * @returns size in bytes, 0 if there is no content yet */ - async getSize() { + getSize() { return this.#resource.getSize(); } /** * Adds a resource collection name that was involved in locating this resource. * - * @param {string} name Resource collection name + * @param name Resource collection name */ - pushCollection(name) { + pushCollection(name: string) { return this.#resource.pushCollection(name); } /** * Tracing: Get tree for printing out trace * - * @returns {object} Trace tree + * @returns Trace tree */ getPathTree() { return this.#resource.getPathTree(); @@ -202,8 +190,7 @@ class ResourceFacade { * [MiddlewareUtil]{@link module:@ui5/server.middleware.MiddlewareUtil}, which will * return a Specification Version-compatible Project interface. * - * @public - * @returns {@ui5/project/specifications/Project} Project this resource is associated with + * @returns Project this resource is associated with */ getProject() { return this.#resource.getProject(); @@ -212,27 +199,25 @@ class ResourceFacade { /** * Assign a project to the resource * - * @public - * @param {@ui5/project/specifications/Project} project Project this resource is associated with + * @param project Project this resource is associated with */ - setProject(project) { + setProject(project: Project) { return this.#resource.setProject(project); } /** * Check whether a project has been assigned to the resource * - * @public - * @returns {boolean} True if the resource is associated with a project + * @returns True if the resource is associated with a project */ hasProject() { return this.#resource.hasProject(); } + /** * Check whether the content of this resource has been changed during its life cycle * - * @public - * @returns {boolean} True if the resource's content has been changed + * @returns True if the resource's content has been changed */ isModified() { return this.#resource.isModified(); @@ -242,17 +227,16 @@ class ResourceFacade { * Returns source metadata if any where provided during the creation of this resource. * Typically set by an adapter to store information for later retrieval. * - * @returns {object|null} + * @returns */ getSourceMetadata() { return this.#resource.getSourceMetadata(); } - /** * Returns the resource concealed by this facade * - * @returns {@ui5/fs/Resource} + * @returns */ getConcealedResource() { return this.#resource; diff --git a/lib/ResourceTagCollection.js b/src/ResourceTagCollection.ts similarity index 65% rename from lib/ResourceTagCollection.js rename to src/ResourceTagCollection.ts index 9214c15b..992bc3a9 100644 --- a/lib/ResourceTagCollection.js +++ b/src/ResourceTagCollection.ts @@ -1,16 +1,30 @@ const tagNamespaceRegExp = /^[a-z][a-z0-9]+$/; // part before the colon const tagNameRegExp = /^[A-Z][A-Za-z0-9]+$/; // part after the colon +import {type ResourceInterface} from "./Resource.js"; import ResourceFacade from "./ResourceFacade.js"; +interface PathTagsInterface { + [key: string]: string | number | boolean | undefined | PathTagsInterface; +}; /** - * A ResourceTagCollection * - * @private - * @class - * @alias @ui5/fs/internal/ResourceTagCollection + * @param elem Variable to test if it's with a PathTagsInterface type + */ +export function isPathTagsInterface(elem: unknown): elem is PathTagsInterface { + return typeof elem === "object"; +} + +/** + * A ResourceTagCollection */ class ResourceTagCollection { - constructor({allowedTags = [], allowedNamespaces = [], tags}) { + _allowedTags: string[]; + _allowedNamespaces: string[]; + _pathTags: PathTagsInterface; + _allowedNamespacesRegExp: null | RegExp; + + constructor({allowedTags = [], allowedNamespaces = [], tags}: + {allowedTags?: string[]; allowedNamespaces?: string[]; tags?: PathTagsInterface}) { this._allowedTags = allowedTags; // Allowed tags are validated during use this._allowedNamespaces = allowedNamespaces; @@ -29,35 +43,42 @@ class ResourceTagCollection { this._allowedNamespacesRegExp = null; } - this._pathTags = tags || Object.create(null); + this._pathTags = tags ?? Object.create(null) as PathTagsInterface; } - setTag(resourcePath, tag, value = true) { + setTag(resourcePath: ResourceInterface | string, tag: string, value: string | number | boolean = true) { resourcePath = this._getPath(resourcePath); this._validateTag(tag); this._validateValue(value); if (!this._pathTags[resourcePath]) { - this._pathTags[resourcePath] = Object.create(null); + this._pathTags[resourcePath] = Object.create(null) as PathTagsInterface; + } + + const pointer = this._pathTags[resourcePath]; + if (isPathTagsInterface(pointer)) { + pointer[tag] = value; } - this._pathTags[resourcePath][tag] = value; } - clearTag(resourcePath, tag) { + clearTag(resourcePath: ResourceInterface | string, tag: string) { resourcePath = this._getPath(resourcePath); this._validateTag(tag); - if (this._pathTags[resourcePath]) { - this._pathTags[resourcePath][tag] = undefined; + const pointer = this._pathTags[resourcePath]; + if (isPathTagsInterface(pointer)) { + pointer[tag] = undefined; } } - getTag(resourcePath, tag) { + getTag(resourcePath: ResourceInterface | string, + tag: string): string | number | boolean | undefined | PathTagsInterface { resourcePath = this._getPath(resourcePath); this._validateTag(tag); - if (this._pathTags[resourcePath]) { - return this._pathTags[resourcePath][tag]; + const pointer = this._pathTags[resourcePath]; + if (isPathTagsInterface(pointer)) { + return pointer[tag]; } } @@ -65,14 +86,14 @@ class ResourceTagCollection { return this._pathTags; } - acceptsTag(tag) { + acceptsTag(tag: string) { if (this._allowedTags.includes(tag) || this._allowedNamespacesRegExp?.test(tag)) { return true; } return false; } - _getPath(resourcePath) { + _getPath(resourcePath: ResourceInterface | string): string { if (typeof resourcePath !== "string") { if (resourcePath instanceof ResourceFacade) { resourcePath = resourcePath.getConcealedResource().getPath(); @@ -86,7 +107,7 @@ class ResourceTagCollection { return resourcePath; } - _validateTag(tag) { + _validateTag(tag: string) { if (!tag.includes(":")) { throw new Error(`Invalid Tag "${tag}": Colon required after namespace`); } @@ -112,7 +133,7 @@ class ResourceTagCollection { } } - _validateValue(value) { + _validateValue(value: string | number | boolean) { const type = typeof value; if (!["string", "number", "boolean"].includes(type)) { throw new Error( diff --git a/lib/WriterCollection.js b/src/WriterCollection.ts similarity index 56% rename from lib/WriterCollection.js rename to src/WriterCollection.ts index 7c01f783..e6b62369 100644 --- a/lib/WriterCollection.js +++ b/src/WriterCollection.ts @@ -1,22 +1,23 @@ import AbstractReaderWriter from "./AbstractReaderWriter.js"; import ReaderCollection from "./ReaderCollection.js"; import escapeStringRegExp from "escape-string-regexp"; +import type Trace from "./tracing/Trace.js"; +import {type ResourceInterface} from "./Resource.js"; /** * Resource Locator WriterCollection - * - * @public - * @class - * @alias @ui5/fs/WriterCollection - * @extends @ui5/fs/AbstractReaderWriter */ class WriterCollection extends AbstractReaderWriter { + _basePathRegex: string; + _writerMapping: Record; + _readerCollection: ReaderCollection; + /** * The constructor. * - * @param {object} parameters Parameters - * @param {string} parameters.name The collection name - * @param {object.} parameters.writerMapping + * @param parameters Parameters + * @param parameters.name The collection name + * @param parameters.writerMapping * Mapping of virtual base paths to writers. Path are matched greedy * * @example @@ -28,7 +29,7 @@ class WriterCollection extends AbstractReaderWriter { * } * }); */ - constructor({name, writerMapping}) { + constructor({name, writerMapping}: {name: string; writerMapping: Record}) { super(name); if (!writerMapping) { @@ -41,7 +42,7 @@ class WriterCollection extends AbstractReaderWriter { // Create a regular expression (which is greedy by nature) from all paths to easily // find the correct writer for any given resource path - this._basePathRegex = basePaths.sort().reduce((regex, basePath, idx) => { + this._basePathRegex = basePaths.sort().reduce((regex, basePath) => { // Validate base path if (!basePath) { throw new Error(`Empty path in path mapping of WriterCollection ${this._name}`); @@ -61,46 +62,55 @@ class WriterCollection extends AbstractReaderWriter { this._writerMapping = writerMapping; this._readerCollection = new ReaderCollection({ name: `Reader collection of writer collection '${this._name}'`, - readers: Object.values(writerMapping) + readers: Object.values(writerMapping), }); } /** * Locates resources by glob. * - * @private - * @param {string|string[]} pattern glob pattern as string or an array of + * @param pattern glob pattern as string or an array of * glob patterns for virtual directory structure - * @param {object} options glob options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources + * @param options glob options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to list of resources */ - _byGlob(pattern, options, trace) { + _byGlob(pattern: string | string[], options: {nodir: boolean}, trace: Trace) { return this._readerCollection._byGlob(pattern, options, trace); } /** * Locates resources by path. * - * @private - * @param {string} virPath Virtual path - * @param {object} options Options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource + * @param virPath Virtual path + * @param options Options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to a single resource */ - _byPath(virPath, options, trace) { + _byPath(virPath: string, options: {nodir: boolean}, trace: Trace) { return this._readerCollection._byPath(virPath, options, trace); } /** * Writes the content of a resource to a path. * - * @private - * @param {@ui5/fs/Resource} resource The Resource to write - * @param {object} [options] Write options, see above - * @returns {Promise} Promise resolving once data has been written + * @param resource The Resource to write + * @param [options] Write options, see above + * @param [options.drain] Whether the resource content shall be emptied during the write process. + * Do not use in conjunction with the readOnly option. + * Activating this option might improve overall memory consumption. + * This should be used in cases where this is the last access to the resource. + * E.g. the final write of a resource after all processing is finished. + * @param [options.readOnly] Whether the resource content shall be written read-only + * Do not use in conjunction with the drain option. + * The written file will be used as the new source of this resources content. + * Therefore the written file should not be altered by any means. + * Activating this option might improve overall memory consumption. + * @returns Promise resolving once data has been written */ - _write(resource, options) { + _write(resource: ResourceInterface, options?: {drain?: boolean; readOnly?: boolean}) { const resourcePath = resource.getPath(); const basePathMatch = resourcePath.match(this._basePathRegex); diff --git a/lib/adapters/AbstractAdapter.js b/src/adapters/AbstractAdapter.ts similarity index 69% rename from lib/adapters/AbstractAdapter.js rename to src/adapters/AbstractAdapter.ts index 96cf4154..36c89023 100644 --- a/lib/adapters/AbstractAdapter.js +++ b/src/adapters/AbstractAdapter.ts @@ -4,29 +4,32 @@ const log = getLogger("resources:adapters:AbstractAdapter"); import {minimatch} from "minimatch"; import micromatch from "micromatch"; import AbstractReaderWriter from "../AbstractReaderWriter.js"; -import Resource from "../Resource.js"; +import Resource, {type Resource_Options, type LegacyResource, type ResourceInterface} from "../Resource.js"; +import type {Project} from "@ui5/project/specifications/Project"; +import type Trace from "../tracing/Trace.js"; +import {isMigratedResource} from "../utils/tsUtils.js"; /** * Abstract Resource Adapter - * - * @abstract - * @public - * @class - * @alias @ui5/fs/adapters/AbstractAdapter - * @extends @ui5/fs/AbstractReaderWriter */ class AbstractAdapter extends AbstractReaderWriter { + _virBasePath: string; + _virBaseDir: string; + _excludes: string[]; + _excludesNegated: string[]; + _project?: Project; + /** * The constructor * - * @public - * @param {object} parameters Parameters - * @param {string} parameters.virBasePath + * @param parameters Parameters + * @param parameters.virBasePath * Virtual base path. Must be absolute, POSIX-style, and must end with a slash - * @param {string[]} [parameters.excludes] List of glob patterns to exclude - * @param {object} [parameters.project] Experimental, internal parameter. Do not use + * @param [parameters.excludes] List of glob patterns to exclude + * @param [parameters.project] Experimental, internal parameter. Do not use */ - constructor({virBasePath, excludes = [], project}) { + constructor({virBasePath, excludes = [], project}: + {virBasePath: string; excludes?: string[]; project?: Project}) { if (new.target === AbstractAdapter) { throw new TypeError("Class 'AbstractAdapter' is abstract"); } @@ -48,19 +51,18 @@ class AbstractAdapter extends AbstractReaderWriter { this._excludesNegated = excludes.map((pattern) => `!${pattern}`); this._project = project; } + /** * Locates resources by glob. * - * @abstract - * @private - * @param {string|string[]} virPattern glob pattern as string or an array of + * @param virPattern glob pattern as string or an array of * glob patterns for virtual directory structure - * @param {object} [options={}] glob options - * @param {boolean} [options.nodir=true] Do not match directories - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources + * @param [options] glob options + * @param [options.nodir] Do not match directories + * @param trace Trace instance + * @returns Promise resolving to list of resources */ - async _byGlob(virPattern, options = {nodir: true}, trace) { + async _byGlob(virPattern: string | string[], options = {nodir: true}, trace: Trace): Promise { const excludes = this._excludesNegated; if (!(virPattern instanceof Array)) { @@ -69,8 +71,8 @@ class AbstractAdapter extends AbstractReaderWriter { // Append static exclude patterns virPattern = Array.prototype.concat.apply(virPattern, excludes); - let patterns = virPattern.map(this._normalizePattern, this); - patterns = Array.prototype.concat.apply([], patterns); + const normalizedPatterns = virPattern.map(this._normalizePattern.bind(this)); + const patterns = Array.prototype.concat.apply([], normalizedPatterns) as string[]; if (patterns.length === 0) { return []; } @@ -83,15 +85,15 @@ class AbstractAdapter extends AbstractReaderWriter { return [ this._createResource({ statInfo: { // TODO: make closer to fs stat info - isDirectory: function() { + isDirectory: function () { return true; - } + }, }, source: { - adapter: "Abstract" + adapter: "Abstract", }, - path: subPath - }) + path: subPath, + }), ]; } } @@ -102,10 +104,10 @@ class AbstractAdapter extends AbstractReaderWriter { /** * Validate if virtual path should be excluded * - * @param {string} virPath Virtual Path - * @returns {boolean} True if path is excluded, otherwise false + * @param virPath Virtual Path + * @returns True if path is excluded, otherwise false */ - _isPathExcluded(virPath) { + _isPathExcluded(virPath: string[]) { return micromatch(virPath, this._excludes).length > 0; } @@ -114,10 +116,10 @@ class AbstractAdapter extends AbstractReaderWriter { * This means that it either starts with the virtual base path of the adapter * or equals the base directory (base path without a trailing slash) * - * @param {string} virPath Virtual Path - * @returns {boolean} True if path should be handled + * @param virPath Virtual Path + * @returns True if path should be handled */ - _isPathHandled(virPath) { + _isPathHandled(virPath: string) { // Check whether path starts with base path, or equals base directory return virPath.startsWith(this._virBasePath) || virPath === this._virBaseDir; } @@ -125,29 +127,27 @@ class AbstractAdapter extends AbstractReaderWriter { /** * Normalizes virtual glob patterns. * - * @private - * @param {string} virPattern glob pattern for virtual directory structure - * @returns {string[]} A list of normalized glob patterns + * @param virPattern glob pattern for virtual directory structure + * @returns A list of normalized glob patterns */ - _normalizePattern(virPattern) { - const that = this; + _normalizePattern(virPattern: string) { const mm = new minimatch.Minimatch(virPattern); const basePathParts = this._virBaseDir.split("/"); - function matchSubset(subset) { + const matchSubset = (subset: (string | typeof minimatch.GLOBSTAR | RegExp | undefined)[]) => { let i; for (i = 0; i < basePathParts.length; i++) { const globPart = subset[i]; if (globPart === undefined) { log.verbose("Ran out of glob parts to match (this should not happen):"); - if (that._project) { // project is optional - log.verbose(`Project: ${that._project.getName()}`); + if (this._project) { // project is optional + log.verbose(`Project: ${this._project.getName()}`); } - log.verbose(`Virtual base path: ${that._virBaseDir}`); + log.verbose(`Virtual base path: ${this._virBaseDir}`); log.verbose(`Pattern to match: ${virPattern}`); log.verbose(`Current subset (tried index ${i}):`); - log.verbose(subset); + log.verbose(String(subset)); return {idx: i, virtualMatch: true}; } const basePathPart = basePathParts[i]; @@ -171,9 +171,9 @@ class AbstractAdapter extends AbstractReaderWriter { return {rootMatch: true}; } return {idx: i}; - } + }; - const resultGlobs = []; + const resultGlobs: string[] = []; for (let i = 0; i < mm.set.length; i++) { const match = matchSubset(mm.set[i]); if (match) { @@ -197,34 +197,34 @@ class AbstractAdapter extends AbstractReaderWriter { return resultGlobs; } - _createResource(parameters) { + _createResource(parameters: Resource_Options): ResourceInterface { if (this._project) { parameters.project = this._project; } return new Resource(parameters); } - _migrateResource(resource) { + _migrateResource(resource: LegacyResource | ResourceInterface): Promise | ResourceInterface { // This function only returns a promise if a migration is necessary. // Since this is rarely the case, we therefore reduce the amount of // created Promises by making this differentiation // Check if its a fs/Resource v3, function 'hasProject' was // introduced with v3 therefore take it as the indicator - if (resource.hasProject) { + if (isMigratedResource(resource)) { return resource; } return this._createFromLegacyResource(resource); } - async _createFromLegacyResource(resource) { + async _createFromLegacyResource(resource: LegacyResource): Promise { const options = { path: resource._path, statInfo: resource._statInfo, - source: resource._source - }; + source: resource._source, + } as Resource_Options; - if (resource._stream) { + if (resource._stream && resource._getBufferFromStream) { options.buffer = await resource._getBufferFromStream(); } else if (resource._createStream) { options.createStream = resource._createStream; @@ -234,14 +234,14 @@ class AbstractAdapter extends AbstractReaderWriter { return new Resource(options); } - _assignProjectToResource(resource) { + _assignProjectToResource(resource: ResourceInterface) { if (this._project) { // Assign project to resource if necessary if (resource.hasProject()) { if (resource.getProject() !== this._project) { throw new Error( `Unable to write resource associated with project ` + - `${resource.getProject().getName()} into adapter of project ${this._project.getName()}: ` + + `${resource.getProject()?.getName()} into adapter of project ${this._project.getName()}: ` + resource.getPath()); } return; @@ -251,7 +251,7 @@ class AbstractAdapter extends AbstractReaderWriter { } } - _resolveVirtualPathToBase(inputVirPath, writeMode = false) { + _resolveVirtualPathToBase(inputVirPath: string, writeMode = false): string | null { if (!path.isAbsolute(inputVirPath)) { throw new Error(`Failed to resolve virtual path '${inputVirPath}': Path must be absolute`); } @@ -265,15 +265,15 @@ class AbstractAdapter extends AbstractReaderWriter { if (!this._isPathHandled(virPath)) { if (log.isLevelEnabled("silly")) { log.silly(`Failed to resolve virtual path '${inputVirPath}': ` + - `Resolved path does not start with adapter base path '${this._virBasePath}' or equals ` + - `base dir: ${this._virBaseDir}`); + `Resolved path does not start with adapter base path '${this._virBasePath}' or equals ` + + `base dir: ${this._virBaseDir}`); } return null; } - if (this._isPathExcluded(virPath)) { + if (this._isPathExcluded(Array.isArray(virPath) ? virPath : [virPath])) { if (log.isLevelEnabled("silly")) { log.silly(`Failed to resolve virtual path '${inputVirPath}': ` + - `Resolved path is excluded by configuration of adapter with base path '${this._virBasePath}'`); + `Resolved path is excluded by configuration of adapter with base path '${this._virBasePath}'`); } return null; } diff --git a/lib/adapters/FileSystem.js b/src/adapters/FileSystem.ts similarity index 77% rename from lib/adapters/FileSystem.js rename to src/adapters/FileSystem.ts index f7862caa..eb3f7919 100644 --- a/lib/adapters/FileSystem.js +++ b/src/adapters/FileSystem.ts @@ -7,35 +7,41 @@ const copyFile = promisify(fs.copyFile); const chmod = promisify(fs.chmod); const mkdir = promisify(fs.mkdir); const stat = promisify(fs.stat); -import {globby, isGitIgnored} from "globby"; +import {globby, type GlobbyFilterFunction, isGitIgnored} from "globby"; import {PassThrough} from "node:stream"; import AbstractAdapter from "./AbstractAdapter.js"; +import type {Project} from "@ui5/project/specifications/Project"; +import type Trace from "../tracing/Trace.js"; +import {type LegacyResource, type Resource_Options, type ResourceInterface} from "../Resource.js"; +import {isError} from "../utils/tsUtils.js"; const READ_ONLY_MODE = 0o444; const ADAPTER_NAME = "FileSystem"; + /** * File system resource adapter - * - * @public - * @class - * @alias @ui5/fs/adapters/FileSystem - * @extends @ui5/fs/adapters/AbstractAdapter */ class FileSystem extends AbstractAdapter { + _fsBasePath: string; + _useGitignore: boolean; + _isGitIgnored!: GlobbyFilterFunction; + /** * The Constructor. * - * @param {object} parameters Parameters - * @param {string} parameters.virBasePath + * @param parameters Parameters + * @param parameters.virBasePath * Virtual base path. Must be absolute, POSIX-style, and must end with a slash - * @param {string} parameters.fsBasePath + * @param parameters.fsBasePath * File System base path. Must be absolute and must use platform-specific path segment separators - * @param {string[]} [parameters.excludes] List of glob patterns to exclude - * @param {object} [parameters.useGitignore=false] + * @param [parameters.excludes] List of glob patterns to exclude + * @param [parameters.useGitignore] * Whether to apply any excludes defined in an optional .gitignore in the given fsBasePath directory - * @param {@ui5/project/specifications/Project} [parameters.project] Project this adapter belongs to (if any) + * @param [parameters.project] Project this adapter belongs to (if any) */ - constructor({virBasePath, project, fsBasePath, excludes, useGitignore=false}) { + constructor({virBasePath, project, fsBasePath, excludes, useGitignore = false}: + {virBasePath: string; project?: Project; fsBasePath: string; excludes?: string[]; useGitignore?: boolean} + ) { super({virBasePath, project, excludes}); if (!fsBasePath) { @@ -51,14 +57,13 @@ class FileSystem extends AbstractAdapter { /** * Locate resources by glob. * - * @private - * @param {Array} patterns Array of glob patterns - * @param {object} [options={}] glob options - * @param {boolean} [options.nodir=true] Do not match directories - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources + * @param patterns Array of glob patterns + * @param [options] glob options + * @param [options.nodir] Do not match directories + * @param trace Trace instance + * @returns Promise resolving to list of resources */ - async _runGlob(patterns, options = {nodir: true}, trace) { + async _runGlob(patterns: string[], options = {nodir: true}, trace: Trace) { const opt = { cwd: this._fsBasePath, dot: true, @@ -68,7 +73,7 @@ class FileSystem extends AbstractAdapter { }; trace.globCall(); - const promises = []; + const promises: Promise[] = []; if (!opt.onlyFiles && patterns.includes("")) { // Match physical root directory promises.push(new Promise((resolve, reject) => { fs.stat(this._fsBasePath, (err, stat) => { @@ -81,11 +86,11 @@ class FileSystem extends AbstractAdapter { path: this._virBaseDir, sourceMetadata: { adapter: ADAPTER_NAME, - fsPath: this._fsBasePath + fsPath: this._fsBasePath, }, createStream: () => { return fs.createReadStream(this._fsBasePath); - } + }, })); } }); @@ -112,7 +117,7 @@ class FileSystem extends AbstractAdapter { `the configured virtual base path of the adapter. Base path: '${this._virBasePath}'`); resolve(null); } - const fsPath = this._resolveToFileSystem(relPath); + const fsPath = this._resolveToFileSystem(relPath ?? ""); // Workaround for not getting the stat from the glob fs.stat(fsPath, (err, stat) => { @@ -125,11 +130,11 @@ class FileSystem extends AbstractAdapter { path: virPath, sourceMetadata: { adapter: ADAPTER_NAME, - fsPath: fsPath + fsPath: fsPath, }, createStream: () => { return fs.createReadStream(fsPath); - } + }, })); } }); @@ -139,19 +144,19 @@ class FileSystem extends AbstractAdapter { const results = await Promise.all(promises); // Flatten results - return Array.prototype.concat.apply([], results).filter(($) => $); + return Array.prototype.concat.apply([], results).filter(($) => $) as ResourceInterface[]; } /** * Locate a resource by path. * - * @private - * @param {string} virPath Absolute virtual path - * @param {object} options Options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource or null if not found + * @param virPath Absolute virtual path + * @param options Options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to a single resource or null if not found */ - async _byPath(virPath, options, trace) { + async _byPath(virPath: string, options: {nodir: boolean}, trace: Trace) { const relPath = this._resolveVirtualPathToBase(virPath); if (relPath === null) { @@ -162,11 +167,11 @@ class FileSystem extends AbstractAdapter { return this._createResource({ project: this._project, statInfo: { // TODO: make closer to fs stat info - isDirectory: function() { + isDirectory: function () { return true; - } + }, }, - path: virPath + path: virPath, }); } else { return null; @@ -180,7 +185,7 @@ class FileSystem extends AbstractAdapter { if (this._useGitignore) { if (!this._isGitIgnored) { this._isGitIgnored = await isGitIgnored({ - cwd: this._fsBasePath + cwd: this._fsBasePath, }); } // Check whether path should be ignored @@ -195,26 +200,26 @@ class FileSystem extends AbstractAdapter { if (options.nodir && statInfo.isDirectory()) { return null; } - const resourceOptions = { + const resourceOptions: Resource_Options = { project: this._project, statInfo, path: virPath, sourceMetadata: { adapter: ADAPTER_NAME, - fsPath - } + fsPath, + }, }; if (!statInfo.isDirectory()) { // Add content as lazy stream - resourceOptions.createStream = function() { + resourceOptions.createStream = function () { return fs.createReadStream(fsPath); }; } return this._createResource(resourceOptions); } catch (err) { - if (err.code === "ENOENT") { // "File or directory does not exist" + if (isError(err) && err.code === "ENOENT") { // "File or directory does not exist" return null; } else { throw err; @@ -225,27 +230,30 @@ class FileSystem extends AbstractAdapter { /** * Writes the content of a resource to a path. * - * @private - * @param {@ui5/fs/Resource} resource Resource to write - * @param {object} [options] - * @param {boolean} [options.readOnly] Whether the resource content shall be written read-only + * @param anyResource Resource to write + * @param [options] Writer options + * @param [options.readOnly] Whether the resource content shall be written read-only * Do not use in conjunction with the drain option. * The written file will be used as the new source of this resources content. * Therefore the written file should not be altered by any means. * Activating this option might improve overall memory consumption. - * @param {boolean} [options.drain] Whether the resource content shall be emptied during the write process. + * @param [options.drain] Whether the resource content shall be emptied during the write process. * Do not use in conjunction with the readOnly option. * Activating this option might improve overall memory consumption. * This should be used in cases where this is the last access to the resource. * E.g. the final write of a resource after all processing is finished. - * @returns {Promise} Promise resolving once data has been written + * @returns Promise resolving once data has been written */ - async _write(resource, {drain, readOnly}) { - resource = this._migrateResource(resource); - if (resource instanceof Promise) { + async _write(anyResource: LegacyResource | ResourceInterface, + {drain, readOnly}: {drain?: boolean; readOnly?: boolean}) { + const potentialResourceP = this._migrateResource(anyResource); + let resource: ResourceInterface; + if (potentialResourceP instanceof Promise) { // Only await if the migrate function returned a promise // Otherwise await would automatically create a Promise, causing unwanted overhead - resource = await resource; + resource = await potentialResourceP; + } else { + resource = potentialResourceP; } this._assignProjectToResource(resource); if (drain && readOnly) { @@ -253,7 +261,7 @@ class FileSystem extends AbstractAdapter { "Do not use options 'drain' and 'readOnly' at the same time."); } - const relPath = this._resolveVirtualPathToBase(resource.getPath(), true); + const relPath = this._resolveVirtualPathToBase(resource.getPath(), true) ?? ""; const fsPath = this._resolveToFileSystem(relPath); const dirPath = path.dirname(fsPath); @@ -307,11 +315,11 @@ class FileSystem extends AbstractAdapter { } else { // Transform stream into buffer before writing contentStream = new PassThrough(); - const buffers = []; + const buffers: Buffer[] = []; contentStream.on("error", (err) => { reject(err); }); - contentStream.on("data", (data) => { + contentStream.on("data", (data: Buffer) => { buffers.push(data); }); contentStream.on("end", () => { @@ -321,7 +329,7 @@ class FileSystem extends AbstractAdapter { resource.getStream().pipe(contentStream); } - const writeOptions = {}; + const writeOptions: {mode?: number} = {}; if (readOnly) { writeOptions.mode = READ_ONLY_MODE; } @@ -330,8 +338,8 @@ class FileSystem extends AbstractAdapter { write.on("error", (err) => { reject(err); }); - write.on("close", (ex) => { - resolve(); + write.on("close", () => { + resolve(undefined); }); contentStream.pipe(write); }); @@ -347,13 +355,13 @@ class FileSystem extends AbstractAdapter { // This should be identical to buffering the resource content in memory, since the written file // can not be modified. // We chose this approach to be more memory efficient in scenarios where readOnly is used - resource.setStream(function() { + resource.setStream(function () { return fs.createReadStream(fsPath); }); } } - _resolveToFileSystem(relPath) { + _resolveToFileSystem(relPath: string) { const fsPath = path.join(this._fsBasePath, relPath); if (!fsPath.startsWith(this._fsBasePath)) { diff --git a/lib/adapters/Memory.js b/src/adapters/Memory.ts similarity index 53% rename from lib/adapters/Memory.js rename to src/adapters/Memory.ts index 35be99cf..af6d71dc 100644 --- a/lib/adapters/Memory.js +++ b/src/adapters/Memory.ts @@ -2,56 +2,56 @@ import {getLogger} from "@ui5/logger"; const log = getLogger("resources:adapters:Memory"); import micromatch from "micromatch"; import AbstractAdapter from "./AbstractAdapter.js"; +import {type Project} from "@ui5/project/specifications/Project"; +import {type LegacyResource, type ResourceInterface} from "../Resource.js"; +import type Trace from "../tracing/Trace.js"; const ADAPTER_NAME = "Memory"; /** * Virtual resource Adapter - * - * @public - * @class - * @alias @ui5/fs/adapters/Memory - * @extends @ui5/fs/adapters/AbstractAdapter */ class Memory extends AbstractAdapter { + _virFiles: Record; + _virDirs: Record; + /** * The constructor. * - * @public - * @param {object} parameters Parameters - * @param {string} parameters.virBasePath + * @param parameters Parameters + * @param parameters.virBasePath * Virtual base path. Must be absolute, POSIX-style, and must end with a slash - * @param {string[]} [parameters.excludes] List of glob patterns to exclude - * @param {@ui5/project/specifications/Project} [parameters.project] Project this adapter belongs to (if any) + * @param [parameters.excludes] List of glob patterns to exclude + * @param [parameters.project] Project this adapter belongs to (if any) */ - constructor({virBasePath, project, excludes}) { + constructor({virBasePath, project, excludes}: {virBasePath: string; project?: Project; excludes?: string[]}) { super({virBasePath, project, excludes}); - this._virFiles = Object.create(null); // map full of files - this._virDirs = Object.create(null); // map full of directories + this._virFiles = Object.create(null) as Record; // map full of files + this._virDirs = Object.create(null) as Record; // map full of directories } /** * Matches and returns resources from a given map (either _virFiles or _virDirs). * - * @private - * @param {string[]} patterns - * @param {object} resourceMap - * @returns {Promise} + * @param patterns glob patterns + * @param resourceMap Resources cache + * @returns */ - async _matchPatterns(patterns, resourceMap) { + async _matchPatterns(patterns: string[], + resourceMap: Record): Promise { const resourcePaths = Object.keys(resourceMap); const matchedPaths = micromatch(resourcePaths, patterns, { - dot: true + dot: true, }); return await Promise.all(matchedPaths.map((virPath) => { - const resource = resourceMap[virPath]; + const resource: ResourceInterface = resourceMap[virPath]; if (resource) { return this._cloneResource(resource); } - })); + }).filter(($) => !!$)); } - async _cloneResource(resource) { + async _cloneResource(resource: ResourceInterface): Promise { const clonedResource = await resource.clone(); if (this._project) { clonedResource.setProject(this._project); @@ -62,28 +62,27 @@ class Memory extends AbstractAdapter { /** * Locate resources by glob. * - * @private - * @param {Array} patterns array of glob patterns - * @param {object} [options={}] glob options - * @param {boolean} [options.nodir=true] Do not match directories - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources + * @param patterns array of glob patterns + * @param [options] glob options + * @param [options.nodir] Do not match directories + * @param _trace Trace instance + * @returns Promise resolving to list of resources */ - async _runGlob(patterns, options = {nodir: true}, trace) { + async _runGlob(patterns: string[], options = {nodir: true}, _trace: Trace) { if (patterns[0] === "" && !options.nodir) { // Match virtual root directory return [ this._createResource({ project: this._project, statInfo: { // TODO: make closer to fs stat info - isDirectory: function() { + isDirectory: function () { return true; - } + }, }, sourceMetadata: { - adapter: ADAPTER_NAME + adapter: ADAPTER_NAME, }, - path: this._virBasePath.slice(0, -1) - }) + path: this._virBasePath.slice(0, -1), + }), ]; } @@ -100,13 +99,13 @@ class Memory extends AbstractAdapter { /** * Locates resources by path. * - * @private - * @param {string} virPath Virtual path - * @param {object} options Options - * @param {@ui5/fs/tracing.Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource + * @param virPath Virtual path + * @param options Options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to a single resource */ - async _byPath(virPath, options, trace) { + async _byPath(virPath: string, options: {nodir: boolean}, trace: Trace) { const relPath = this._resolveVirtualPathToBase(virPath); if (relPath === null) { return null; @@ -114,9 +113,9 @@ class Memory extends AbstractAdapter { trace.pathCall(); - const resource = this._virFiles[relPath]; + const resource: ResourceInterface = this._virFiles[relPath]; - if (!resource || (options.nodir && resource.getStatInfo().isDirectory())) { + if (!resource || (options.nodir && resource.getStatInfo().isDirectory?.())) { return null; } else { return await this._cloneResource(resource); @@ -126,25 +125,27 @@ class Memory extends AbstractAdapter { /** * Writes the content of a resource to a path. * - * @private - * @param {@ui5/fs/Resource} resource The Resource to write - * @returns {Promise} Promise resolving once data has been written + * @param anyResource The Resource to write + * @returns Promise resolving once data has been written */ - async _write(resource) { - resource = this._migrateResource(resource); - if (resource instanceof Promise) { + async _write(anyResource: ResourceInterface | LegacyResource) { + const migratedResource = this._migrateResource(anyResource); + let resource: ResourceInterface; + if (migratedResource instanceof Promise) { // Only await if the migrate function returned a promise // Otherwise await would automatically create a Promise, causing unwanted overhead - resource = await resource; + resource = await migratedResource; + } else { + resource = migratedResource; } this._assignProjectToResource(resource); const relPath = this._resolveVirtualPathToBase(resource.getPath(), true); log.silly(`Writing to virtual path ${resource.getPath()}`); - this._virFiles[relPath] = await resource.clone(); + this._virFiles[relPath!] = await resource.clone(); // Add virtual directories for all path segments of the written resource // TODO: Add tests for all this - const pathSegments = relPath.split("/"); + const pathSegments = relPath!.split("/"); pathSegments.pop(); // Remove last segment representing the resource itself pathSegments.forEach((segment, i) => { @@ -160,14 +161,14 @@ class Memory extends AbstractAdapter { this._virDirs[segment] = this._createResource({ project: this._project, sourceMetadata: { - adapter: ADAPTER_NAME + adapter: ADAPTER_NAME, }, statInfo: { // TODO: make closer to fs stat info - isDirectory: function() { + isDirectory: function () { return true; - } + }, }, - path: this._virBasePath + segment + path: this._virBasePath + segment, }); } } diff --git a/src/fsInterface.ts b/src/fsInterface.ts new file mode 100644 index 00000000..bdaece5b --- /dev/null +++ b/src/fsInterface.ts @@ -0,0 +1,114 @@ +import type AbstractReader from "./AbstractReader.js"; +import type * as fs from "node:fs"; +import {type Buffer} from "node:buffer"; + +/** + * + * @param inputPath Path to convert to POSIX + */ +function toPosix(inputPath: string) { + return inputPath.replace(/\\/g, "/"); +} + +// Define the types for the options parameter +type Read_File_Options = {encoding?: string} | string | undefined; + +// Define the types for the callback functions +type Read_File_Callback = (err: Error | null, data?: Buffer | string) => void; +type Stat_Callback = (err: Error | null, stats?: fs.Stats) => void; +type Readdir_Callback = (err: Error | null, files?: string[]) => void; +type Mkdir_Callback = (err?: Error | null) => void; + +/** + */ +interface File_System_Interface { + readFile: (fsPath: string, options: Read_File_Options, callback: Read_File_Callback) => void; + stat: (fsPath: string, callback: Stat_Callback) => void; + readdir: (fsPath: string, callback: Readdir_Callback) => void; + mkdir: (fsPath: string, callback: Mkdir_Callback) => void; +}; + +/** + * Wraps readers to access them through a [Node.js fs]{@link https://nodejs.org/api/fs.html} styled interface. + * + * @param reader Resource Reader or Collection + * + * @returns Object with [Node.js fs]{@link https://nodejs.org/api/fs.html} styled functions + * [readFile]{@link https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback}, + * [stat]{@link https://nodejs.org/api/fs.html#fs_fs_stat_path_options_callback}, + * [readdir]{@link https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback} and + * [mkdir]{@link https://nodejs.org/api/fs.html#fs_fs_mkdir_path_options_callback} + */ +function fsInterface(reader: AbstractReader) { + const fileSystem: File_System_Interface = { + readFile(fsPath, options, callback) { + if (typeof options === "function") { + callback = options; + options = undefined; + } + if (typeof options === "string") { + options = {encoding: options}; + } + const posixPath = toPosix(fsPath); + reader.byPath(posixPath, { + nodir: false, + }).then(function (resource) { + if (!resource) { + const error: NodeJS.ErrnoException = + new Error(`ENOENT: no such file or directory, open '${fsPath}'`); + error.code = "ENOENT"; // "File or directory does not exist" + callback(error); + return; + } + + return resource.getBuffer().then(function (buffer) { + let res; + + if (options?.encoding) { + res = buffer.toString(options.encoding as BufferEncoding); + } else { + res = buffer; + } + + callback(null, res); + }); + }).catch(callback); + }, + stat(fsPath, callback) { + const posixPath = toPosix(fsPath); + void reader.byPath(posixPath, { + nodir: false, + }).then(function (resource) { + if (!resource) { + const error: NodeJS.ErrnoException = + new Error(`ENOENT: no such file or directory, stat '${fsPath}'`); + error.code = "ENOENT"; // "File or directory does not exist" + callback(error); + } else { + callback(null, resource.getStatInfo() as fs.StatsBase); + } + }).catch(callback); + }, + readdir(fsPath, callback) { + let posixPath = toPosix(fsPath); + if (!(/\/$/.exec(posixPath))) { + // Add trailing slash if not present + posixPath += "/"; + } + void reader.byGlob(posixPath + "*", { + nodir: false, + }).then((resources) => { + const files = resources.map((resource) => { + return resource.getName(); + }); + callback(null, files); + }).catch(callback); + }, + mkdir(_fsPath, callback) { + setTimeout(callback, 0); + }, + }; + + return fileSystem; +} +export default fsInterface; diff --git a/src/readers/Filter.ts b/src/readers/Filter.ts new file mode 100644 index 00000000..0e0b63d2 --- /dev/null +++ b/src/readers/Filter.ts @@ -0,0 +1,78 @@ +import AbstractReader from "../AbstractReader.js"; +import {type ResourceInterface} from "../Resource.js"; +import type Trace from "../tracing/Trace.js"; + +/** + * Filter callback + * + * @param resource Resource to test + * @returns Whether to keep the resource + */ +type Filter_Callback = (resource: ResourceInterface) => boolean; + +export interface Filter_Params { + reader: AbstractReader; + callback: Filter_Callback; +}; + +/** + * A reader that allows dynamic filtering of resources passed through it + */ +class Filter extends AbstractReader { + _reader: AbstractReader; + _callback: (resource: ResourceInterface) => boolean; + + /** + * Constructor + * + * @param parameters Parameters + * @param parameters.reader The resource reader or collection to wrap + * @param parameters.callback + * Filter function. Will be called for every resource read through this reader. + */ + constructor({reader, callback}: Filter_Params) { + super(); + if (!reader) { + throw new Error(`Missing parameter "reader"`); + } + if (!callback) { + throw new Error(`Missing parameter "callback"`); + } + this._reader = reader; + this._callback = callback; + } + + /** + * Locates resources by glob. + * + * @param pattern glob pattern as string or an array of + * glob patterns for virtual directory structure + * @param options glob options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to list of resources + */ + async _byGlob(pattern: string | string[], options: {nodir: boolean}, trace: Trace) { + const result = await this._reader._byGlob(pattern, options, trace); + return result.filter(this._callback); + } + + /** + * Locates resources by path. + * + * @param virPath Virtual path + * @param options Options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to a single resource + */ + async _byPath(virPath: string, options: {nodir: boolean}, trace: Trace) { + const result = await this._reader._byPath(virPath, options, trace); + if (result && !this._callback(result)) { + return null; + } + return result; + } +} + +export default Filter; diff --git a/lib/readers/Link.js b/src/readers/Link.ts similarity index 56% rename from lib/readers/Link.js rename to src/readers/Link.ts index dd2d40b3..01e9d44c 100644 --- a/lib/readers/Link.js +++ b/src/readers/Link.ts @@ -3,6 +3,15 @@ import ResourceFacade from "../ResourceFacade.js"; import {prefixGlobPattern} from "../resourceFactory.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("resources:readers:Link"); +import type Trace from "../tracing/Trace.js"; + +export interface Link_Args { + reader: AbstractReader; + pathMapping: { + linkPath: string; + targetPath: string; + }; +}; /** * A reader that allows for rewriting paths segments of all resources passed through it. @@ -20,31 +29,27 @@ const log = getLogger("resources:readers:Link"); * // The following resolves with a @ui5/fs/ResourceFacade of the resource * // located at "/resources/my-app-name/Component.js" in the sourceReader * const resource = await linkedReader.byPath("/app/Component.js"); - * - * @public - * @class - * @alias @ui5/fs/readers/Link - * @extends @ui5/fs/AbstractReader */ class Link extends AbstractReader { + _reader: AbstractReader; + /** * Path mapping for a [Link]{@link @ui5/fs/readers/Link} * - * @public - * @typedef {object} @ui5/fs/readers/Link/PathMapping - * @property {string} linkPath Path to match and replace in the requested path or pattern - * @property {string} targetPath Path to use as a replacement in the request for the source reader + * linkPath Path to match and replace in the requested path or pattern + * + * targetPath Path to use as a replacement in the request for the source reader */ + _pathMapping: {linkPath: string; targetPath: string}; /** * Constructor * - * @public - * @param {object} parameters Parameters - * @param {@ui5/fs/AbstractReader} parameters.reader The resource reader or collection to wrap - * @param {@ui5/fs/readers/Link/PathMapping} parameters.pathMapping + * @param parameters Parameters + * @param parameters.reader The resource reader or collection to wrap + * @param parameters.pathMapping Path mapping for a [Link]{@link @ui5/fs/readers/Link} */ - constructor({reader, pathMapping}) { + constructor({reader, pathMapping}: Link_Args) { super(); if (!reader) { throw new Error(`Missing parameter "reader"`); @@ -60,18 +65,19 @@ class Link extends AbstractReader { /** * Locates resources by glob. * - * @private - * @param {string|string[]} patterns glob pattern as string or an array of + * @param pattern glob pattern as string or an array of * glob patterns for virtual directory structure - * @param {object} options glob options - * @param {@ui5/fs/tracing/Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources + * @param options glob options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to list of resources */ - async _byGlob(patterns, options, trace) { - if (!(patterns instanceof Array)) { - patterns = [patterns]; + async _byGlob(pattern: string | string[], options: {nodir: boolean}, trace: Trace) { + if (!(pattern instanceof Array)) { + pattern = [pattern]; } - patterns = patterns.map((pattern) => { + + pattern = pattern.flatMap((pattern) => { if (pattern.startsWith(this._pathMapping.linkPath)) { pattern = pattern.substr(this._pathMapping.linkPath.length); } @@ -79,31 +85,34 @@ class Link extends AbstractReader { }); // Flatten prefixed patterns - patterns = Array.prototype.concat.apply([], patterns); + pattern = Array.prototype.concat.apply([], pattern); // Keep resource's internal path unchanged for now - const resources = await this._reader._byGlob(patterns, options, trace); - return resources.map((resource) => { - const resourcePath = resource.getPath(); - if (resourcePath.startsWith(this._pathMapping.targetPath)) { - return new ResourceFacade({ - resource, - path: this._pathMapping.linkPath + resourcePath.substr(this._pathMapping.targetPath.length) - }); - } - }); + const resources = await this._reader._byGlob(pattern, options, trace); + + return resources + .map((resource) => { + const resourcePath = resource.getPath(); + if (resourcePath.startsWith(this._pathMapping.targetPath)) { + return new ResourceFacade({ + resource, + path: this._pathMapping.linkPath + resourcePath.substr(this._pathMapping.targetPath.length), + }); + } + }) + .filter((resource) => resource !== undefined); } /** * Locates resources by path. * - * @private - * @param {string} virPath Virtual path - * @param {object} options Options - * @param {@ui5/fs/tracing/Trace} trace Trace instance - * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource + * @param virPath Virtual path + * @param options Options + * @param options.nodir Do not match directories + * @param trace Trace instance + * @returns Promise resolving to a single resource */ - async _byPath(virPath, options, trace) { + async _byPath(virPath: string, options: {nodir: boolean}, trace: Trace) { if (!virPath.startsWith(this._pathMapping.linkPath)) { return null; } @@ -114,13 +123,13 @@ class Link extends AbstractReader { if (resource) { return new ResourceFacade({ resource, - path: this._pathMapping.linkPath + resource.getPath().substr(this._pathMapping.targetPath.length) + path: this._pathMapping.linkPath + resource.getPath().substr(this._pathMapping.targetPath.length), }); } return null; } - static _validatePathMapping({linkPath, targetPath}) { + static _validatePathMapping({linkPath, targetPath}: {linkPath: string; targetPath: string}) { if (!linkPath) { throw new Error(`Path mapping is missing attribute "linkPath"`); } diff --git a/lib/resourceFactory.js b/src/resourceFactory.ts similarity index 56% rename from lib/resourceFactory.js rename to src/resourceFactory.ts index ba48d76d..99da6188 100644 --- a/lib/resourceFactory.js +++ b/src/resourceFactory.ts @@ -5,17 +5,19 @@ import FsAdapter from "./adapters/FileSystem.js"; import MemAdapter from "./adapters/Memory.js"; import ReaderCollection from "./ReaderCollection.js"; import ReaderCollectionPrioritized from "./ReaderCollectionPrioritized.js"; -import Resource from "./Resource.js"; +import Resource, {type Resource_Options, type ResourceInterface} from "./Resource.js"; import WriterCollection from "./WriterCollection.js"; -import Filter from "./readers/Filter.js"; -import Link from "./readers/Link.js"; +import Filter, {type Filter_Params} from "./readers/Filter.js"; +import Link, {type Link_Args} from "./readers/Link.js"; import {getLogger} from "@ui5/logger"; +import {type Project} from "@ui5/project/specifications/Project"; +import type AbstractReader from "./AbstractReader.js"; +import type AbstractReaderWriter from "./AbstractReaderWriter.js"; const log = getLogger("resources:resourceFactory"); /** * @module @ui5/fs/resourceFactory * @description A collection of resource related APIs - * @public */ /** @@ -24,21 +26,22 @@ const log = getLogger("resources:resourceFactory"); * If a file system base path is given, file system resource ReaderWriter is returned. * In any other case a virtual one. * - * @public - * @param {object} parameters Parameters - * @param {string} parameters.virBasePath Virtual base path. Must be absolute, POSIX-style, and must end with a slash - * @param {string} [parameters.fsBasePath] + * @param parameters Parameters + * @param parameters.virBasePath Virtual base path. Must be absolute, POSIX-style, and must end with a slash + * @param [parameters.fsBasePath] * File System base path. * If this parameter is supplied, a File System adapter will be created instead of a Memory adapter. * The provided path must be absolute and must use platform-specific path segment separators. - * @param {string[]} [parameters.excludes] List of glob patterns to exclude - * @param {object} [parameters.useGitignore=false] + * @param [parameters.excludes] List of glob patterns to exclude + * @param [parameters.useGitignore] * Whether to apply any excludes defined in an optional .gitignore in the base directory. * This parameter only takes effect in conjunction with the fsBasePath parameter. - * @param {@ui5/project/specifications/Project} [parameters.project] Project this adapter belongs to (if any) - * @returns {@ui5/fs/adapters/FileSystem|@ui5/fs/adapters/Memory} File System- or Virtual Adapter + * @param [parameters.project] Project this adapter belongs to (if any) + * @returns File System- or Virtual Adapter */ -export function createAdapter({fsBasePath, virBasePath, project, excludes, useGitignore}) { +export function createAdapter({fsBasePath, virBasePath, project, excludes, useGitignore}: +{fsBasePath?: string; virBasePath: string; project?: Project; excludes?: string[]; useGitignore?: boolean} +) { if (fsBasePath) { return new FsAdapter({fsBasePath, virBasePath, project, excludes, useGitignore}); } else { @@ -49,17 +52,18 @@ export function createAdapter({fsBasePath, virBasePath, project, excludes, useGi /** * Creates a File System adapter and wraps it in a ReaderCollection * - * @public - * @param {object} parameters Parameters - * @param {string} parameters.virBasePath Virtual base path. Must be absolute, POSIX-style, and must end with a slash - * @param {string} parameters.fsBasePath + * @param parameters Parameters + * @param parameters.virBasePath Virtual base path. Must be absolute, POSIX-style, and must end with a slash + * @param parameters.fsBasePath * File System base path. Must be absolute and must use platform-specific path segment separators - * @param {object} [parameters.project] Experimental, internal parameter. Do not use - * @param {string[]} [parameters.excludes] List of glob patterns to exclude - * @param {string} [parameters.name] Name for the reader collection - * @returns {@ui5/fs/ReaderCollection} Reader collection wrapping an adapter + * @param [parameters.project] Experimental, internal parameter. Do not use + * @param [parameters.excludes] List of glob patterns to exclude + * @param [parameters.name] Name for the reader collection + * @returns Reader collection wrapping an adapter */ -export function createReader({fsBasePath, virBasePath, project, excludes = [], name}) { +export function createReader({fsBasePath, virBasePath, project, excludes = [], name}: +{fsBasePath: string; virBasePath: string; project?: Project; excludes?: string[]; name?: string} +) { if (!fsBasePath) { // Creating a reader with a memory adapter seems pointless right now // since there would be no way to fill the adapter with resources @@ -73,7 +77,7 @@ export function createReader({fsBasePath, virBasePath, project, excludes = [], n // ui5 runtime path of the excluded resources. Therefore, only allow paths like /resources//test // starting with specVersion 4.0 if (excludes.length && project && project.getType() === "application") { - normalizedExcludes = excludes.map((pattern) => { + const nestedNormalizedExcludes = excludes.map((pattern) => { if (pattern.startsWith(virBasePath) || pattern.startsWith("!" + virBasePath)) { return pattern; } @@ -82,9 +86,9 @@ export function createReader({fsBasePath, virBasePath, project, excludes = [], n return prefixGlobPattern(pattern, virBasePath); }); // Flatten list of patterns - normalizedExcludes = Array.prototype.concat.apply([], normalizedExcludes); + normalizedExcludes = Array.prototype.concat.apply([], nestedNormalizedExcludes) as string[]; log.verbose(`Effective exclude patterns for application project ${project.getName()}:\n` + - normalizedExcludes.join(", ")); + normalizedExcludes.join(", ")); } return new ReaderCollection({ name, @@ -92,58 +96,56 @@ export function createReader({fsBasePath, virBasePath, project, excludes = [], n fsBasePath, virBasePath, project, - excludes: normalizedExcludes - })] + excludes: normalizedExcludes, + })], }); } /** * Creates a ReaderCollection * - * @public - * @param {object} parameters Parameters - * @param {string} parameters.name The collection name - * @param {@ui5/fs/AbstractReader[]} parameters.readers List of resource readers (all tried in parallel) - * @returns {@ui5/fs/ReaderCollection} Reader collection wrapping provided readers + * @param parameters Parameters + * @param parameters.name The collection name + * @param parameters.readers List of resource readers (all tried in parallel) + * @returns Reader collection wrapping provided readers */ -export function createReaderCollection({name, readers}) { +export function createReaderCollection({name, readers}: {name: string; readers: AbstractReader[]}) { return new ReaderCollection({ name, - readers + readers, }); } /** * Creates a ReaderCollectionPrioritized * - * @public - * @param {object} parameters - * @param {string} parameters.name The collection name - * @param {@ui5/fs/AbstractReader[]} parameters.readers Prioritized list of resource readers + * @param parameters Parameters + * @param parameters.name The collection name + * @param parameters.readers Prioritized list of resource readers * (first is tried first) - * @returns {@ui5/fs/ReaderCollectionPrioritized} Reader collection wrapping provided readers + * @returns Reader collection wrapping provided readers */ -export function createReaderCollectionPrioritized({name, readers}) { +export function createReaderCollectionPrioritized({name, readers}: {name: string; readers: AbstractReader[]}) { return new ReaderCollectionPrioritized({ name, - readers + readers, }); } /** * Creates a WriterCollection * - * @public - * @param {object} parameters - * @param {string} parameters.name The collection name - * @param {object.} parameters.writerMapping Mapping of virtual base + * @param parameters Parameters + * @param parameters.name The collection name + * @param parameters.writerMapping Mapping of virtual base * paths to writers. Path are matched greedy - * @returns {@ui5/fs/WriterCollection} Writer collection wrapping provided writers + * @returns Writer collection wrapping provided writers */ -export function createWriterCollection({name, writerMapping}) { +export function createWriterCollection({name, writerMapping}: +{name: string; writerMapping: Record}) { return new WriterCollection({ name, - writerMapping + writerMapping, }); } @@ -151,11 +153,10 @@ export function createWriterCollection({name, writerMapping}) { * Creates a [Resource]{@link @ui5/fs/Resource}. * Accepts the same parameters as the [Resource]{@link @ui5/fs/Resource} constructor. * - * @public - * @param {object} parameters Parameters to be passed to the resource constructor - * @returns {@ui5/fs/Resource} Resource + * @param parameters Parameters to be passed to the resource constructor + * @returns Resource */ -export function createResource(parameters) { +export function createResource(parameters: Resource_Options): ResourceInterface { return new Resource(parameters); } @@ -166,26 +167,27 @@ export function createResource(parameters) { * to write modified files into a separate writer, this is usually a Memory adapter. If a file already exists it is * fetched from the memory to work on it in further build steps. * - * @public - * @param {object} parameters - * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers - * @param {@ui5/fs/AbstractReaderWriter} [parameters.writer] A ReaderWriter instance which is + * @param parameters Parameters + * @param parameters.reader Single reader or collection of readers + * @param [parameters.writer] A ReaderWriter instance which is * only used for writing files. If not supplied, a Memory adapter will be created. - * @param {string} [parameters.name="workspace"] Name of the collection - * @param {string} [parameters.virBasePath="/"] Virtual base path - * @returns {@ui5/fs/DuplexCollection} DuplexCollection which wraps the provided resource locators + * @param [parameters.name] Name of the collection + * @param [parameters.virBasePath] Virtual base path + * @returns DuplexCollection which wraps the provided resource locators */ -export function createWorkspace({reader, writer, virBasePath = "/", name = "workspace"}) { +export function createWorkspace({reader, writer, virBasePath = "/", name = "workspace"}: +{reader: AbstractReader; writer?: AbstractReaderWriter; virBasePath?: string; name?: string} +) { if (!writer) { writer = new MemAdapter({ - virBasePath + virBasePath, }); } return new DuplexCollection({ reader, writer, - name + name, }); } @@ -194,14 +196,13 @@ export function createWorkspace({reader, writer, virBasePath = "/", name = "work * The provided callback is called for every resource that is retrieved through the * reader and decides whether the resource shall be passed on or dropped. * - * @public - * @param {object} parameters - * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers - * @param {@ui5/fs/readers/Filter~callback} parameters.callback + * @param parameters Parameters + * @param parameters.reader Single reader or collection of readers + * @param parameters.callback * Filter function. Will be called for every resource passed through this reader. - * @returns {@ui5/fs/readers/Filter} Reader instance + * @returns Reader instance */ -export function createFilterReader(parameters) { +export function createFilterReader(parameters: Filter_Params) { return new Filter(parameters); } @@ -223,13 +224,14 @@ export function createFilterReader(parameters) { * // located at "/resources/my-app-name/Component.js" in the sourceReader * const resource = await linkedReader.byPath("/app/Component.js"); * - * @public - * @param {object} parameters - * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers - * @param {@ui5/fs/readers/Link/PathMapping} parameters.pathMapping - * @returns {@ui5/fs/readers/Link} Reader instance + * @param parameters Parameters + * @param parameters.reader Single reader or collection of readers + * @param parameters.pathMapping Path mapping for a [Link]{@link @ui5/fs/readers/Link} + * @param parameters.pathMapping.linkPath Path to match and replace in the requested path or pattern + * @param parameters.pathMapping.targetPath Path to use as a replacement in the request for the source reader + * @returns Reader instance */ -export function createLinkReader(parameters) { +export function createLinkReader(parameters: Link_Args) { return new Link(parameters); } @@ -240,19 +242,18 @@ export function createLinkReader(parameters) { * This simulates "flat" resource access, which is for example common for projects of type * "application". * - * @public - * @param {object} parameters - * @param {@ui5/fs/AbstractReader} parameters.reader Single reader or collection of readers - * @param {string} parameters.namespace Project namespace - * @returns {@ui5/fs/readers/Link} Reader instance + * @param parameters Parameters + * @param parameters.reader Single reader or collection of readers + * @param parameters.namespace Project namespace + * @returns Reader instance */ -export function createFlatReader({reader, namespace}) { +export function createFlatReader({reader, namespace}: {reader: AbstractReader; namespace: string}) { return new Link({ reader: reader, pathMapping: { linkPath: `/`, - targetPath: `/resources/${namespace}/` - } + targetPath: `/resources/${namespace}/`, + }, }); } @@ -260,14 +261,15 @@ export function createFlatReader({reader, namespace}) { * Normalizes virtual glob patterns by prefixing them with * a given virtual base directory path * - * @param {string} virPattern glob pattern for virtual directory structure - * @param {string} virBaseDir virtual base directory path to prefix the given patterns with - * @returns {string[]} A list of normalized glob patterns + * @param virPattern glob pattern for virtual directory structure + * @param virBaseDir virtual base directory path to prefix the given patterns with + * @returns A list of normalized glob patterns */ -export function prefixGlobPattern(virPattern, virBaseDir) { +export function prefixGlobPattern(virPattern: string, virBaseDir: string): string[] { const mm = new minimatch.Minimatch(virPattern); const resultGlobs = []; + // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < mm.globSet.length; i++) { let resultPattern = path.posix.join(virBaseDir, mm.globSet[i]); diff --git a/lib/tracing/Trace.js b/src/tracing/Trace.ts similarity index 81% rename from lib/tracing/Trace.js rename to src/tracing/Trace.ts index 30ac9e19..756ff0b0 100644 --- a/lib/tracing/Trace.js +++ b/src/tracing/Trace.ts @@ -3,17 +3,20 @@ const log = getLogger("resources:tracing:Trace"); const logGlobs = getLogger("resources:tracing:Trace:globs"); const logPaths = getLogger("resources:tracing:Trace:paths"); import prettyHrtime from "pretty-hrtime"; -import summaryTrace from "./traceSummary.js"; -const hasOwnProperty = Object.prototype.hasOwnProperty; +import summaryTrace, {type CollectionsType} from "./traceSummary.js"; /** * Trace * - * @private - * @class */ class Trace { - constructor(name) { + _name!: string; + _startTime!: [number, number]; + _globCalls!: number; + _pathCalls!: number; + _collections!: CollectionsType; + + constructor(name: string) { if (!log.isLevelEnabled("silly")) { return; } @@ -21,7 +24,7 @@ class Trace { this._startTime = process.hrtime(); this._globCalls = 0; this._pathCalls = 0; - this._collections = Object.create(null); + this._collections = Object.create(null) as CollectionsType; summaryTrace.traceStarted(); } @@ -41,7 +44,7 @@ class Trace { summaryTrace.pathCall(); } - collection(name) { + collection(name: string) { if (!log.isLevelEnabled("silly")) { return; } @@ -50,7 +53,7 @@ class Trace { this._collections[name].calls++; } else { this._collections[name] = { - calls: 1 + calls: 1, }; } summaryTrace.collection(name); @@ -76,7 +79,7 @@ class Trace { report += ` ${colCount} reader-collections involed:\n`; for (const coll in this._collections) { - if (hasOwnProperty.call(this._collections, coll)) { + if (Object.prototype.hasOwnProperty.call(this._collections, coll)) { report += ` ${this._collections[coll].calls}x ${coll}\n`; } } @@ -90,7 +93,7 @@ class Trace { logPaths.silly(report); } - summaryTrace.traceEnded(); + void summaryTrace.traceEnded(); } } diff --git a/lib/tracing/traceSummary.js b/src/tracing/traceSummary.ts similarity index 65% rename from lib/tracing/traceSummary.js rename to src/tracing/traceSummary.ts index 79f89378..2e23755c 100644 --- a/lib/tracing/traceSummary.js +++ b/src/tracing/traceSummary.ts @@ -2,31 +2,54 @@ import {getLogger} from "@ui5/logger"; const log = getLogger("resources:tracing:total"); import prettyHrtime from "pretty-hrtime"; -const hasOwnProperty = Object.prototype.hasOwnProperty; -let timeoutId; +let timeoutId: NodeJS.Timeout; let active = false; let tracesRunning = 0; -let traceData; +export type CollectionsType = Record>; + +let traceData: null | { + startTime: [number, number]; + pathCalls: number; + globCalls: number; + collections: CollectionsType; + traceCalls: number; + timeDiff?: [number, number]; +}; + +/** + * + */ function init() { traceData = { startTime: process.hrtime(), pathCalls: 0, globCalls: 0, collections: {}, - traceCalls: 0 + traceCalls: 0, }; active = true; } +/** + * + */ function reset() { traceData = null; active = false; } +/** + * + */ function report() { let report = ""; - const time = prettyHrtime(traceData.timeDiff); + + if (!traceData) { + return; + } + + const time = prettyHrtime(traceData.timeDiff!); const colCount = Object.keys(traceData.collections).length; report += "==========================\n[=> TRACE SUMMARY:\n"; @@ -41,7 +64,7 @@ function report() { report += ` ${colCount} rl-collections involed:\n`; for (const coll in traceData.collections) { - if (hasOwnProperty.call(traceData.collections, coll)) { + if (Object.prototype.hasOwnProperty.call(traceData.collections, coll)) { report += ` ${traceData.collections[coll].calls}x ${coll}\n`; } } @@ -49,6 +72,9 @@ function report() { log.silly(report); } +/** + * + */ function someTraceStarted() { if (!log.isLevelEnabled("silly")) { return; @@ -57,15 +83,18 @@ function someTraceStarted() { init(); } tracesRunning++; - traceData.traceCalls++; + traceData!.traceCalls++; if (timeoutId) { clearTimeout(timeoutId); } } -function someTraceEnded() { - return new Promise(function(resolve, reject) { +/** + * + */ +function someTraceEnded(): Promise { + return new Promise(function (resolve) { if (!active) { resolve(); return; @@ -79,8 +108,8 @@ function someTraceEnded() { if (timeoutId) { clearTimeout(timeoutId); } - traceData.timeDiff = process.hrtime(traceData.startTime); - timeoutId = setTimeout(function() { + traceData!.timeDiff = process.hrtime(traceData!.startTime); + timeoutId = setTimeout(function () { report(); reset(); resolve(); @@ -88,22 +117,37 @@ function someTraceEnded() { }); } +/** + * + */ function pathCall() { if (!active) { return; } - traceData.pathCalls++; + if (traceData) { + traceData.pathCalls++; + } } +/** + * + */ function globCall() { if (!active) { return; } - traceData.globCalls++; + + if (traceData) { + traceData.globCalls++; + } } -function collection(name) { - if (!active) { +/** + * + * @param name TraceData collection name + */ +function collection(name: string) { + if (!active || !traceData) { return; } const collection = traceData.collections[name]; @@ -111,7 +155,7 @@ function collection(name) { traceData.collections[name].calls++; } else { traceData.collections[name] = { - calls: 1 + calls: 1, }; } } @@ -121,5 +165,5 @@ export default { globCall: globCall, collection: collection, traceStarted: someTraceStarted, - traceEnded: someTraceEnded + traceEnded: someTraceEnded, }; diff --git a/src/utils/mock-projects.d.ts b/src/utils/mock-projects.d.ts new file mode 100644 index 00000000..60219504 --- /dev/null +++ b/src/utils/mock-projects.d.ts @@ -0,0 +1,21 @@ +// TODO: This file is meant only for temp resolve of the UI5 tooling +// dependencies, until they got migrated and we can have the real TS definitions + +declare module "@ui5/project/specifications/Project" { + + export interface Project { + getName: () => string; + getVersion: () => string; + getType: () => "project" | "application" | "library"; + } +} + +declare module "@ui5/logger" { + interface logger { + silly(x: string): void; + verbose(x: string): void; + isLevelEnabled(x: string): boolean; + } + + export function getLogger(x: string): logger; +} diff --git a/src/utils/tsUtils.ts b/src/utils/tsUtils.ts new file mode 100644 index 00000000..1a08f40b --- /dev/null +++ b/src/utils/tsUtils.ts @@ -0,0 +1,28 @@ +import {type ResourceInterface} from "../Resource.js"; +import type Resource from "../Resource.js"; + +/** + * + * @param testString Variable to test if it's a string type + */ +export function isString(testString: unknown): testString is string { + return testString instanceof String || String(testString) === testString; +} + +/** + * + * @param resource Variable to test if it's with a Resource type + */ +export function isMigratedResource(resource: unknown): resource is Resource | ResourceInterface { + // Check if its a fs/Resource v3, function 'hasProject' was + // introduced with v3 therefore take it as the indicator + return !!resource && typeof resource === "object" && ("hasProject" in resource); +} + +/** + * + * @param error Error to test + */ +export function isError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error; +} diff --git a/src/utils/types/clone.d.ts b/src/utils/types/clone.d.ts new file mode 100644 index 00000000..c6862463 --- /dev/null +++ b/src/utils/types/clone.d.ts @@ -0,0 +1,3 @@ +declare module "clone" { + export default function clone(arg: T): T; +} diff --git a/src/utils/types/graceful-fs.d.ts b/src/utils/types/graceful-fs.d.ts new file mode 100644 index 00000000..ca3301bf --- /dev/null +++ b/src/utils/types/graceful-fs.d.ts @@ -0,0 +1,6 @@ +// "graceful-fs" definitions just inherit the ones from node:fs, but are quite +// inconvenient. So, proxying the ones from node:fs would be better. +declare module "graceful-fs" { + import * as fs from "node:fs"; + export default fs; +} diff --git a/test/lib/AbstractReader.js b/test/lib/AbstractReader.js deleted file mode 100644 index c5d9c009..00000000 --- a/test/lib/AbstractReader.js +++ /dev/null @@ -1,37 +0,0 @@ -import test from "ava"; -import AbstractReader from "../../lib/AbstractReader.js"; - -test("AbstractReader: constructor throws an error", (t) => { - t.throws(() => { - new AbstractReader(); - }, { - instanceOf: TypeError, - message: "Class 'AbstractReader' is abstract" - }); -}); - -test("Incomplete AbstractReader subclass: Abstract functions throw error", (t) => { - class Dummy extends AbstractReader {} - - const instance = new Dummy(); - t.throws(() => { - instance._byGlob(); - }, { - instanceOf: Error, - message: "Function '_byGlob' is not implemented" - }); - - t.throws(() => { - instance._runGlob(); - }, { - instanceOf: Error, - message: "Function '_runGlob' is not implemented" - }); - - t.throws(() => { - instance._byPath(); - }, { - instanceOf: Error, - message: "Function '_byPath' is not implemented" - }); -}); diff --git a/test/lib/AbstractReader.ts b/test/lib/AbstractReader.ts new file mode 100644 index 00000000..1945ae51 --- /dev/null +++ b/test/lib/AbstractReader.ts @@ -0,0 +1,40 @@ +import test from "ava"; +import AbstractReader from "../../src/AbstractReader.js"; + +test("AbstractReader: constructor throws an error", (t) => { + t.throws(() => { + new AbstractReader(); + }, { + instanceOf: TypeError, + message: "Class 'AbstractReader' is abstract", + }); +}); + +test("Incomplete AbstractReader subclass: Abstract functions throw error", async (t) => { + class Dummy extends AbstractReader {} + + const instance = new Dummy(); + await t.throwsAsync(async () => { + // @ts-expect-error testing invalid value + await instance._byGlob(); + }, { + instanceOf: Error, + message: "Function '_byGlob' is not implemented", + }); + + await t.throwsAsync(async () => { + // @ts-expect-error testing invalid value + await instance._runGlob(); + }, { + instanceOf: Error, + message: "Function '_runGlob' is not implemented", + }); + + await t.throwsAsync(async () => { + // @ts-expect-error testing invalid value + await instance._byPath(); + }, { + instanceOf: Error, + message: "Function '_byPath' is not implemented", + }); +}); diff --git a/test/lib/AbstractReaderWriter.js b/test/lib/AbstractReaderWriter.ts similarity index 65% rename from test/lib/AbstractReaderWriter.js rename to test/lib/AbstractReaderWriter.ts index 1cb5cec2..a81b9a0e 100644 --- a/test/lib/AbstractReaderWriter.js +++ b/test/lib/AbstractReaderWriter.ts @@ -1,12 +1,12 @@ import test from "ava"; -import AbstractReaderWriter from "../../lib/AbstractReaderWriter.js"; +import AbstractReaderWriter from "../../src/AbstractReaderWriter.js"; test("AbstractReaderWriter: constructor throws an error", (t) => { t.throws(() => { new AbstractReaderWriter(); }, { instanceOf: TypeError, - message: "Class 'AbstractReaderWriter' is abstract" + message: "Class 'AbstractReaderWriter' is abstract", }); }); @@ -16,10 +16,10 @@ test("Incomplete AbstractReaderWriter subclass: Abstract functions throw error", const instance = new Dummy(); t.throws(() => { - instance._write(); + // @ts-expect-error testing invalid value + void instance._write(); }, { instanceOf: Error, - message: "Not implemented" + message: "Not implemented", }); }); - diff --git a/test/lib/DuplexCollection.js b/test/lib/DuplexCollection.ts similarity index 85% rename from test/lib/DuplexCollection.js rename to test/lib/DuplexCollection.ts index bba5b096..8d25e8b5 100644 --- a/test/lib/DuplexCollection.js +++ b/test/lib/DuplexCollection.ts @@ -1,14 +1,14 @@ import test from "ava"; import sinon from "sinon"; -import DuplexCollection from "../../lib/DuplexCollection.js"; -import ReaderCollectionPrioritized from "../../lib/ReaderCollectionPrioritized.js"; -import Resource from "../../lib/Resource.js"; +import DuplexCollection from "../../src/DuplexCollection.js"; +import ReaderCollectionPrioritized from "../../src/ReaderCollectionPrioritized.js"; +import Resource from "../../src/Resource.js"; test("DuplexCollection: constructor", (t) => { const duplexCollection = new DuplexCollection({ name: "myCollection", reader: {}, - writer: {} + writer: {}, }); t.deepEqual(duplexCollection._reader, {}, "reader assigned"); @@ -21,7 +21,7 @@ test("DuplexCollection: constructor", (t) => { test("DuplexCollection: constructor with setting default name of an empty string", (t) => { const duplexCollection = new DuplexCollection({ reader: {}, - writer: {} + writer: {}, }); t.deepEqual(duplexCollection._reader, {}, "reader assigned"); @@ -35,15 +35,15 @@ test("DuplexCollection: _byGlob w/o finding a resource", async (t) => { t.plan(3); const abstractReader = { - _byGlob: sinon.stub().returns(Promise.resolve([])) + _byGlob: sinon.stub().returns(Promise.resolve([])), }; const duplexCollection = new DuplexCollection({ name: "myCollection", reader: abstractReader, - writer: abstractReader + writer: abstractReader, }); const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const comboSpy = sinon.spy(duplexCollection._combo, "_byGlob"); @@ -60,18 +60,18 @@ test("DuplexCollection: _byGlob", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const abstractReader = { - _byGlob: sinon.stub().returns(Promise.resolve([resource])) + _byGlob: sinon.stub().returns(Promise.resolve([resource])), }; const duplexCollection = new DuplexCollection({ name: "myCollection", reader: abstractReader, - writer: abstractReader + writer: abstractReader, }); const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const comboSpy = sinon.spy(duplexCollection._combo, "_byGlob"); const resources = await duplexCollection._byGlob("anyPattern", {someOption: true}, trace); @@ -90,19 +90,19 @@ test("DuplexCollection: _byPath with reader finding a resource", async (t) => { const resource = new Resource({ path: "/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const pushCollectionSpy = sinon.spy(resource, "pushCollection"); const abstractReader = { - _byPath: sinon.stub().returns(Promise.resolve(resource)) + _byPath: sinon.stub().returns(Promise.resolve(resource)), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const duplexCollection = new DuplexCollection({ name: "myCollection", reader: abstractReader, - writer: abstractReader + writer: abstractReader, }); const comboSpy = sinon.spy(duplexCollection._combo, "_byPath"); const readResource = await duplexCollection._byPath("anyVirtualPath", {someOption: true}, trace); @@ -119,18 +119,18 @@ test("DuplexCollection: _byPath with two readers both finding no resource", asyn t.plan(3); const abstractReaderOne = { - _byPath: sinon.stub().returns(Promise.resolve()) + _byPath: sinon.stub().returns(Promise.resolve()), }; const abstractReaderTwo = { - _byPath: sinon.stub().returns(Promise.resolve()) + _byPath: sinon.stub().returns(Promise.resolve()), }; const trace = { - collection: sinon.stub() + collection: sinon.stub(), }; const duplexCollection = new DuplexCollection({ name: "myCollection", reader: abstractReaderOne, - writer: abstractReaderTwo + writer: abstractReaderTwo, }); const readResource = await duplexCollection._byPath("anyVirtualPath", {someOption: true}, trace); @@ -146,14 +146,14 @@ test("DuplexCollection: _write successful", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const duplexCollection = new DuplexCollection({ name: "myCollection", reader: {}, writer: { - write: sinon.stub().returns(Promise.resolve()) - } + write: sinon.stub().returns(Promise.resolve()), + }, }); await duplexCollection._write(resource); @@ -164,10 +164,10 @@ test("DuplexCollection: Throws for empty reader", (t) => { t.throws(() => { new DuplexCollection({ name: "myReader", - writer: {} + writer: {}, }); }, { - message: "Cannot create DuplexCollection myReader: No reader provided" + message: "Cannot create DuplexCollection myReader: No reader provided", }); }); @@ -175,9 +175,9 @@ test("DuplexCollection: Throws for empty writer", (t) => { t.throws(() => { new DuplexCollection({ name: "myReader", - reader: {} + reader: {}, }); }, { - message: "Cannot create DuplexCollection myReader: No writer provided" + message: "Cannot create DuplexCollection myReader: No writer provided", }); }); diff --git a/test/lib/ReaderCollection.js b/test/lib/ReaderCollection.ts similarity index 81% rename from test/lib/ReaderCollection.js rename to test/lib/ReaderCollection.ts index d808591b..63949291 100644 --- a/test/lib/ReaderCollection.js +++ b/test/lib/ReaderCollection.ts @@ -1,12 +1,12 @@ import test from "ava"; import sinon from "sinon"; -import ReaderCollection from "../../lib/ReaderCollection.js"; -import Resource from "../../lib/Resource.js"; +import ReaderCollection from "../../src/ReaderCollection.js"; +import Resource from "../../src/Resource.js"; test("ReaderCollection: constructor", (t) => { const readerCollection = new ReaderCollection({ name: "myReader", - readers: [{}, {}, {}] + readers: [{}, {}, {}], }); t.is(readerCollection.getName(), "myReader", "correct name assigned"); @@ -17,14 +17,14 @@ test("ReaderCollection: _byGlob w/o finding a resource", async (t) => { t.plan(4); const abstractReader = { - _byGlob: sinon.stub().returns(Promise.resolve([])) + _byGlob: sinon.stub().returns(Promise.resolve([])), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new ReaderCollection({ name: "myReader", - readers: [abstractReader] + readers: [abstractReader], }); const resources = await readerCollection._byGlob("anyPattern", {someOption: true}, trace); @@ -40,17 +40,17 @@ test("ReaderCollection: _byGlob with finding a resource", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const abstractReader = { - _byGlob: sinon.stub().returns(Promise.resolve([resource])) + _byGlob: sinon.stub().returns(Promise.resolve([resource])), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new ReaderCollection({ name: "myReader", - readers: [abstractReader] + readers: [abstractReader], }); const resources = await readerCollection._byGlob("anyPattern", {someOption: true}, trace); @@ -70,18 +70,18 @@ test("ReaderCollection: _byPath with reader finding a resource", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const pushCollectionSpy = sinon.spy(resource, "pushCollection"); const abstractReader = { - _byPath: sinon.stub().returns(Promise.resolve(resource)) + _byPath: sinon.stub().returns(Promise.resolve(resource)), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new ReaderCollection({ name: "myReader", - readers: [abstractReader] + readers: [abstractReader], }); const readResource = await readerCollection._byPath("anyVirtualPath", {someOption: true}, trace); @@ -99,17 +99,17 @@ test("ReaderCollection: _byPath with two readers both finding no resource", asyn t.plan(4); const abstractReaderOne = { - _byPath: sinon.stub().returns(Promise.resolve()) + _byPath: sinon.stub().returns(Promise.resolve()), }; const abstractReaderTwo = { - _byPath: sinon.stub().returns(Promise.resolve()) + _byPath: sinon.stub().returns(Promise.resolve()), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new ReaderCollection({ name: "myReader", - readers: [abstractReaderOne, abstractReaderTwo] + readers: [abstractReaderOne, abstractReaderTwo], }); const resource = await readerCollection._byPath("anyVirtualPath", {someOption: true}, trace); @@ -124,11 +124,11 @@ test("ReaderCollection: _byPath with two readers both finding no resource", asyn test("ReaderCollection: _byPath with empty readers array", async (t) => { const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new ReaderCollection({ name: "myReader", - readers: [] + readers: [], }); const resource = await readerCollection._byPath("anyVirtualPath", {someOption: true}, trace); @@ -138,21 +138,21 @@ test("ReaderCollection: _byPath with empty readers array", async (t) => { test("ReaderCollection: _byPath with some empty readers", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const abstractReaderOne = { - _byPath: sinon.stub().resolves(resource) + _byPath: sinon.stub().resolves(resource), }; const abstractReaderTwo = { - _byPath: sinon.stub().resolves() + _byPath: sinon.stub().resolves(), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new ReaderCollection({ name: "myReader", - readers: [abstractReaderOne, undefined, abstractReaderTwo] + readers: [abstractReaderOne, undefined, abstractReaderTwo], }); const res = await readerCollection._byPath("anyVirtualPath", {someOption: true}, trace); @@ -161,11 +161,11 @@ test("ReaderCollection: _byPath with some empty readers", async (t) => { test("ReaderCollection: _byGlob with empty readers array", async (t) => { const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new ReaderCollection({ name: "myReader", - readers: [] + readers: [], }); const resource = await readerCollection.byGlob("anyPattern", {someOption: true}, trace); @@ -175,21 +175,21 @@ test("ReaderCollection: _byGlob with empty readers array", async (t) => { test("ReaderCollection: _byGlob with some empty readers", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const abstractReaderOne = { - _byGlob: sinon.stub().resolves([resource]) + _byGlob: sinon.stub().resolves([resource]), }; const abstractReaderTwo = { - _byGlob: sinon.stub().resolves([]) + _byGlob: sinon.stub().resolves([]), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new ReaderCollection({ name: "myReader", - readers: [abstractReaderOne, undefined, abstractReaderTwo] + readers: [abstractReaderOne, undefined, abstractReaderTwo], }); const res = await readerCollection._byGlob("anyVirtualPath", {someOption: true}, trace); diff --git a/test/lib/ReaderCollectionPrioritized.js b/test/lib/ReaderCollectionPrioritized.ts similarity index 81% rename from test/lib/ReaderCollectionPrioritized.js rename to test/lib/ReaderCollectionPrioritized.ts index e13cda12..fe6a4444 100644 --- a/test/lib/ReaderCollectionPrioritized.js +++ b/test/lib/ReaderCollectionPrioritized.ts @@ -1,12 +1,12 @@ import test from "ava"; import sinon from "sinon"; -import ReaderCollectionPrioritized from "../../lib/ReaderCollectionPrioritized.js"; -import Resource from "../../lib/Resource.js"; +import ReaderCollectionPrioritized from "../../src/ReaderCollectionPrioritized.js"; +import Resource from "../../src/Resource.js"; test("ReaderCollectionPrioritized: constructor", (t) => { const readerCollectionPrioritized = new ReaderCollectionPrioritized({ name: "myReader", - readers: [{}, {}, {}] + readers: [{}, {}, {}], }); t.is(readerCollectionPrioritized.getName(), "myReader", "correct name assigned"); @@ -15,14 +15,14 @@ test("ReaderCollectionPrioritized: constructor", (t) => { test("ReaderCollectionPrioritized: _byGlob w/o finding a resource", async (t) => { const abstractReader = { - _byGlob: sinon.stub().returns(Promise.resolve([])) + _byGlob: sinon.stub().returns(Promise.resolve([])), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollectionPrioritized = new ReaderCollectionPrioritized({ name: "myReader", - readers: [abstractReader] + readers: [abstractReader], }); const resources = await readerCollectionPrioritized._byGlob("anyPattern", {someOption: true}, trace); @@ -36,17 +36,17 @@ test("ReaderCollectionPrioritized: _byGlob w/o finding a resource", async (t) => test("ReaderCollectionPrioritized: _byGlob with finding a resource", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const abstractReader = { - _byGlob: sinon.stub().returns(Promise.resolve([resource])) + _byGlob: sinon.stub().returns(Promise.resolve([resource])), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollectionPrioritized = new ReaderCollectionPrioritized({ name: "myReader", - readers: [abstractReader] + readers: [abstractReader], }); const resources = await readerCollectionPrioritized._byGlob("anyPattern", {someOption: true}, trace); @@ -64,18 +64,18 @@ test("ReaderCollectionPrioritized: _byGlob with finding a resource", async (t) = test("ReaderCollectionPrioritized: _byPath with reader finding a resource", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const pushCollectionSpy = sinon.spy(resource, "pushCollection"); const abstractReader = { - _byPath: sinon.stub().returns(Promise.resolve(resource)) + _byPath: sinon.stub().returns(Promise.resolve(resource)), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollectionPrioritized = new ReaderCollectionPrioritized({ name: "myReader", - readers: [abstractReader] + readers: [abstractReader], }); const readResource = await readerCollectionPrioritized._byPath("anyVirtualPath", {someOption: true}, trace); @@ -90,17 +90,17 @@ test("ReaderCollectionPrioritized: _byPath with reader finding a resource", asyn test("ReaderCollectionPrioritized: _byPath with two readers both finding no resource", async (t) => { const abstractReaderOne = { - _byPath: sinon.stub().returns(Promise.resolve()) + _byPath: sinon.stub().returns(Promise.resolve()), }; const abstractReaderTwo = { - _byPath: sinon.stub().returns(Promise.resolve()) + _byPath: sinon.stub().returns(Promise.resolve()), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollectionPrioritized = new ReaderCollectionPrioritized({ name: "myReader", - readers: [abstractReaderOne, abstractReaderTwo] + readers: [abstractReaderOne, abstractReaderTwo], }); const resource = await readerCollectionPrioritized._byPath("anyVirtualPath", {someOption: true}, trace); @@ -114,11 +114,11 @@ test("ReaderCollectionPrioritized: _byPath with two readers both finding no reso test("ReaderCollectionPrioritized: _byPath with empty readers array", async (t) => { const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollectionPrioritized = new ReaderCollectionPrioritized({ name: "myReader", - readers: [] + readers: [], }); const resource = await readerCollectionPrioritized._byPath("anyVirtualPath", {someOption: true}, trace); @@ -128,21 +128,21 @@ test("ReaderCollectionPrioritized: _byPath with empty readers array", async (t) test("ReaderCollectionPrioritized: _byPath with some empty readers", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const abstractReaderOne = { - _byPath: sinon.stub().resolves(resource) + _byPath: sinon.stub().resolves(resource), }; const abstractReaderTwo = { - _byPath: sinon.stub().resolves() + _byPath: sinon.stub().resolves(), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollectionPrioritized = new ReaderCollectionPrioritized({ name: "myReader", - readers: [abstractReaderOne, undefined, abstractReaderTwo] + readers: [abstractReaderOne, undefined, abstractReaderTwo], }); const res = await readerCollectionPrioritized._byPath("anyVirtualPath", {someOption: true}, trace); @@ -151,11 +151,11 @@ test("ReaderCollectionPrioritized: _byPath with some empty readers", async (t) = test("ReaderCollectionPrioritized: _byGlob with empty readers array", async (t) => { const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollectionPrioritized = new ReaderCollectionPrioritized({ name: "myReader", - readers: [] + readers: [], }); const resource = await readerCollectionPrioritized.byGlob("anyPattern", {someOption: true}, trace); @@ -165,21 +165,21 @@ test("ReaderCollectionPrioritized: _byGlob with empty readers array", async (t) test("ReaderCollectionPrioritized: _byGlob with some empty readers", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("content") + buffer: Buffer.from("content"), }); const abstractReaderOne = { - _byGlob: sinon.stub().resolves([resource]) + _byGlob: sinon.stub().resolves([resource]), }; const abstractReaderTwo = { - _byGlob: sinon.stub().resolves([]) + _byGlob: sinon.stub().resolves([]), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollectionPrioritized = new ReaderCollectionPrioritized({ name: "myReader", - readers: [abstractReaderOne, undefined, abstractReaderTwo] + readers: [abstractReaderOne, undefined, abstractReaderTwo], }); const res = await readerCollectionPrioritized._byGlob("anyVirtualPath", {someOption: true}, trace); diff --git a/test/lib/Resource.js b/test/lib/Resource.ts similarity index 86% rename from test/lib/Resource.js rename to test/lib/Resource.ts index aca04728..7c560bc7 100644 --- a/test/lib/Resource.js +++ b/test/lib/Resource.ts @@ -1,19 +1,19 @@ import test from "ava"; -import {Stream, Transform} from "node:stream"; +import {Stream, Transform, type Readable} from "node:stream"; import {promises as fs, createReadStream} from "node:fs"; import path from "node:path"; -import Resource from "../../lib/Resource.js"; +import Resource from "../../src/Resource.js"; +import type {Project} from "@ui5/project/specifications/Project"; function createBasicResource() { const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); const resource = new Resource({ path: "/app/index.html", - createStream: function() { + createStream: function () { return createReadStream(fsPath); }, - project: {}, + project: {} as Project, statInfo: {}, - fsPath }); return resource; } @@ -24,16 +24,16 @@ function createBasicResource() { * @param {stream.Readable} readableStream readable stream * @returns {Promise} resolves with the read string */ -const readStream = (readableStream) => { +const readStream = (readableStream: Readable) => { return new Promise((resolve, reject) => { let streamedResult = ""; - readableStream.on("data", (chunk) => { + readableStream.on("data", (chunk: string) => { streamedResult += chunk; }); readableStream.on("end", () => { resolve(streamedResult); }); - readableStream.on("error", (err) => { + readableStream.on("error", (err: Error) => { reject(err); }); }); @@ -41,10 +41,11 @@ const readStream = (readableStream) => { test("Resource: constructor with missing path parameter", (t) => { t.throws(() => { + // @ts-expect-error testing missing arguments new Resource({}); }, { instanceOf: Error, - message: "Unable to create Resource: Missing parameter 'path'" + message: "Unable to create Resource: Missing parameter 'path'", }); }); @@ -86,7 +87,7 @@ test("Resource: constructor with duplicated content parameter", (t) => { }, { instanceOf: Error, message: "Unable to create Resource: Please set only one content parameter. " + - "'buffer', 'string', 'stream' or 'createStream'" + "'buffer', 'string', 'stream' or 'createStream'", }, "Threw with expected error message"); }); }); @@ -94,7 +95,7 @@ test("Resource: constructor with duplicated content parameter", (t) => { test("Resource: From buffer", async (t) => { const resource = new Resource({ path: "/my/path", - buffer: Buffer.from("Content") + buffer: Buffer.from("Content"), }); t.is(await resource.getSize(), 7, "Content is set"); t.false(resource.isModified(), "Content of new resource is not modified"); @@ -104,7 +105,7 @@ test("Resource: From buffer", async (t) => { test("Resource: From string", async (t) => { const resource = new Resource({ path: "/my/path", - string: "Content" + string: "Content", }); t.is(await resource.getSize(), 7, "Content is set"); t.false(resource.isModified(), "Content of new resource is not modified"); @@ -115,7 +116,7 @@ test("Resource: From stream", async (t) => { const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); const resource = new Resource({ path: "/my/path", - stream: createReadStream(fsPath) + stream: createReadStream(fsPath), }); t.is(await resource.getSize(), 91, "Content is set"); t.false(resource.isModified(), "Content of new resource is not modified"); @@ -128,7 +129,7 @@ test("Resource: From createStream", async (t) => { path: "/my/path", createStream: () => { return createReadStream(fsPath); - } + }, }); t.is(await resource.getSize(), 91, "Content is set"); t.false(resource.isModified(), "Content of new resource is not modified"); @@ -141,8 +142,8 @@ test("Resource: Source metadata", async (t) => { string: "Content", sourceMetadata: { adapter: "My Adapter", - fsPath: "/some/path" - } + fsPath: "/some/path", + }, }); t.is(await resource.getSize(), 7, "Content is set"); t.false(resource.isModified(), "Content of new resource is not modified"); @@ -157,8 +158,8 @@ test("Resource: Source metadata with modified content", async (t) => { sourceMetadata: { adapter: "My Adapter", fsPath: "/some/path", - contentModified: true - } + contentModified: true, + }, }); t.is(await resource.getSize(), 7, "Content is set"); t.false(resource.isModified(), "Content of new resource is not modified"); @@ -175,11 +176,12 @@ test("Resource: Illegal source metadata attribute", (t) => { sourceMetadata: { adapter: "My Adapter", fsPath: "/some/path", - pony: "🦄" - } + // @ts-expect-error testing invalid value + pony: "🦄", + }, }); }, { - message: `Parameter 'sourceMetadata' contains an illegal attribute: pony` + message: `Parameter 'sourceMetadata' contains an illegal attribute: pony`, }, "Threw with expected error message"); }); @@ -188,15 +190,16 @@ test("Resource: Illegal source metadata value", (t) => { new Resource({ path: "/my/path", string: "Content", + // @ts-expect-error testing invalid value sourceMetadata: { adapter: "My Adapter", fsPath: { - some: "value" - } - } + some: "value", + }, + }, }); }, { - message: `Attribute 'fsPath' of parameter 'sourceMetadata' must be of type "string" or "boolean"` + message: `Attribute 'fsPath' of parameter 'sourceMetadata' must be of type "string" or "boolean"`, }, "Threw with expected error message"); }); @@ -207,7 +210,7 @@ test("Resource: getBuffer with throwing an error", (t) => { path: "/my/path/to/resource", }); - return resource.getBuffer().catch(function(error) { + return resource.getBuffer().catch(function (error) { t.is(error.message, "Resource /my/path/to/resource has no content", "getBuffer called w/o having a resource content provided"); }); @@ -216,7 +219,7 @@ test("Resource: getBuffer with throwing an error", (t) => { test("Resource: getPath / getName", (t) => { const resource = new Resource({ path: "/my/path/to/resource.js", - buffer: Buffer.from("Content") + buffer: Buffer.from("Content"), }); t.is(resource.getPath(), "/my/path/to/resource.js", "Correct path"); t.is(resource.getName(), "resource.js", "Correct name"); @@ -225,7 +228,7 @@ test("Resource: getPath / getName", (t) => { test("Resource: setPath / getName", (t) => { const resource = new Resource({ path: "/my/path/to/resource.js", - buffer: Buffer.from("Content") + buffer: Buffer.from("Content"), }); resource.setPath("/my/other/file.json"); t.is(resource.getPath(), "/my/other/file.json", "Correct path"); @@ -235,12 +238,12 @@ test("Resource: setPath / getName", (t) => { test("Resource: setPath with non-absolute path", (t) => { const resource = new Resource({ path: "/my/path/to/resource.js", - buffer: Buffer.from("Content") + buffer: Buffer.from("Content"), }); t.throws(() => { resource.setPath("my/other/file.json"); }, { - message: "Unable to set resource path: Path must be absolute: my/other/file.json" + message: "Unable to set resource path: Path must be absolute: my/other/file.json", }, "Threw with expected error message"); t.is(resource.getPath(), "/my/path/to/resource.js", "Path is unchanged"); t.is(resource.getName(), "resource.js", "Name is unchanged"); @@ -250,10 +253,10 @@ test("Create Resource with non-absolute path", (t) => { t.throws(() => { new Resource({ path: "my/path/to/resource.js", - buffer: Buffer.from("Content") + buffer: Buffer.from("Content"), }); }, { - message: "Unable to set resource path: Path must be absolute: my/path/to/resource.js" + message: "Unable to set resource path: Path must be absolute: my/path/to/resource.js", }, "Threw with expected error message"); }); @@ -262,7 +265,7 @@ test("Resource: getStream", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - buffer: Buffer.from("Content") + buffer: Buffer.from("Content"), }); const result = await readStream(resource.getStream()); @@ -274,7 +277,7 @@ test("Resource: getStream for empty string", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - string: "" + string: "", }); const result = await readStream(resource.getStream()); @@ -286,8 +289,8 @@ test("Resource: getStream for empty string instance", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - // eslint-disable-next-line no-new-wrappers - string: new String("") + + string: new String("") as string, }); const result = await readStream(resource.getStream()); @@ -296,21 +299,21 @@ test("Resource: getStream for empty string instance", async (t) => { test("Resource: getStream throwing an error", (t) => { const resource = new Resource({ - path: "/my/path/to/resource" + path: "/my/path/to/resource", }); t.throws(() => { resource.getStream(); }, { instanceOf: Error, - message: "Resource /my/path/to/resource has no content" + message: "Resource /my/path/to/resource has no content", }); }); test("Resource: setString", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - sourceMetadata: {} // Needs to be passed in order to get the "modified" state + sourceMetadata: {}, // Needs to be passed in order to get the "modified" state }); t.is(resource.getSourceMetadata().contentModified, false, "sourceMetadata modified flag set correctly"); @@ -328,7 +331,7 @@ test("Resource: setString", async (t) => { test("Resource: setBuffer", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - sourceMetadata: {} // Needs to be passed in order to get the "modified" state + sourceMetadata: {}, // Needs to be passed in order to get the "modified" state }); t.is(resource.getSourceMetadata().contentModified, false, "sourceMetadata modified flag set correctly"); @@ -345,7 +348,7 @@ test("Resource: setBuffer", async (t) => { test("Resource: size modification", async (t) => { const resource = new Resource({ - path: "/my/path/to/resource" + path: "/my/path/to/resource", }); t.is(await resource.getSize(), 0, "initial size without content"); @@ -355,7 +358,7 @@ test("Resource: size modification", async (t) => { t.is(await resource.getSize(), 7, "size after manually setting the string"); t.is(await new Resource({ path: "/my/path/to/resource", - string: "Content" + string: "Content", }).getSize(), 7, "size when passing string to constructor"); // buffer @@ -374,7 +377,7 @@ test("Resource: size modification", async (t) => { t.is(await resource.getSize(), 1234, "buffer with alloc after setting the buffer"); t.is(await new Resource({ path: "/my/path/to/resource", - buffer: buf + buffer: buf, }).getSize(), 1234, "buffer with alloc when passing buffer to constructor"); const clonedResource2 = await resource.clone(); @@ -385,7 +388,8 @@ test("Resource: size modification", async (t) => { path: "/my/path/to/resource", }); const stream = new Stream.Readable(); - stream._read = function() {}; + // eslint-disable-next-line @typescript-eslint/no-empty-function + stream._read = function () {}; stream.push("I am a "); stream.push("readable "); stream.push("stream!"); @@ -404,14 +408,15 @@ test("Resource: size modification", async (t) => { test("Resource: setStream (Stream)", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - sourceMetadata: {} // Needs to be passed in order to get the "modified" state + sourceMetadata: {}, // Needs to be passed in order to get the "modified" state }); t.is(resource.getSourceMetadata().contentModified, false, "sourceMetadata modified flag set correctly"); t.false(resource.isModified(), "Resource is not modified"); const stream = new Stream.Readable(); - stream._read = function() {}; + // eslint-disable-next-line @typescript-eslint/no-empty-function + stream._read = function () {}; stream.push("I am a "); stream.push("readable "); stream.push("stream!"); @@ -429,7 +434,7 @@ test("Resource: setStream (Stream)", async (t) => { test("Resource: setStream (Create stream callback)", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - sourceMetadata: {} // Needs to be passed in order to get the "modified" state + sourceMetadata: {}, // Needs to be passed in order to get the "modified" state }); t.is(resource.getSourceMetadata().contentModified, false, "sourceMetadata modified flag set correctly"); @@ -437,7 +442,8 @@ test("Resource: setStream (Create stream callback)", async (t) => { resource.setStream(() => { const stream = new Stream.Readable(); - stream._read = function() {}; + // eslint-disable-next-line @typescript-eslint/no-empty-function + stream._read = function () {}; stream.push("I am a "); stream.push("readable "); stream.push("stream!"); @@ -457,7 +463,7 @@ test("Resource: clone resource with buffer", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - buffer: Buffer.from("Content") + buffer: Buffer.from("Content"), }); const clonedResource = await resource.clone(); @@ -471,10 +477,11 @@ test("Resource: clone resource with stream", async (t) => { t.plan(2); const resource = new Resource({ - path: "/my/path/to/resource" + path: "/my/path/to/resource", }); const stream = new Stream.Readable(); - stream._read = function() {}; + // eslint-disable-next-line @typescript-eslint/no-empty-function + stream._read = function () {}; stream.push("Content"); stream.push(null); @@ -492,8 +499,8 @@ test("Resource: clone resource with sourceMetadata", async (t) => { path: "/my/path/to/resource", sourceMetadata: { adapter: "FileSystem", - fsPath: "/resources/my.js" - } + fsPath: "/resources/my.js", + }, }); const clonedResource = await resource.clone(); @@ -526,18 +533,18 @@ test("Resource: clone resource with sourceMetadata", async (t) => { test("Resource: clone resource with project removes project", async (t) => { const myProject = { - name: "my project" + name: "my project", }; const resource = new Resource({ path: "/my/path/to/resource", - project: myProject + project: myProject as unknown as Project, }); const clonedResource = await resource.clone(); t.pass("Resource cloned"); - const clonedResourceProject = await clonedResource.getProject(); + const clonedResourceProject = clonedResource.getProject(); t.falsy(clonedResourceProject, "Cloned resource should not have a project"); }); @@ -547,8 +554,8 @@ test("Resource: create resource with sourceMetadata.contentModified: true", (t) sourceMetadata: { adapter: "FileSystem", fsPath: "/resources/my.js", - contentModified: true - } + contentModified: true, + }, }); t.true(resource.getSourceMetadata().contentModified, "Modified flag is still true"); @@ -556,7 +563,7 @@ test("Resource: create resource with sourceMetadata.contentModified: true", (t) }); test("getStream with createStream callback content: Subsequent content requests should throw error due " + - "to drained content", async (t) => { +"to drained content", async (t) => { const resource = createBasicResource(); resource.getStream(); t.throws(() => { @@ -567,7 +574,7 @@ test("getStream with createStream callback content: Subsequent content requests }); test("getStream with Buffer content: Subsequent content requests should throw error due to drained " + - "content", async (t) => { +"content", async (t) => { const resource = createBasicResource(); await resource.getBuffer(); resource.getStream(); @@ -579,13 +586,13 @@ test("getStream with Buffer content: Subsequent content requests should throw er }); test("getStream with Stream content: Subsequent content requests should throw error due to drained " + - "content", async (t) => { +"content", async (t) => { const resource = createBasicResource(); const tStream = new Transform({ transform(chunk, encoding, callback) { this.push(chunk.toString()); callback(); - } + }, }); const stream = resource.getStream(); stream.pipe(tStream); @@ -600,13 +607,13 @@ test("getStream with Stream content: Subsequent content requests should throw er }); test("getBuffer from Stream content: Subsequent content requests should not throw error due to drained " + - "content", async (t) => { +"content", async (t) => { const resource = createBasicResource(); const tStream = new Transform({ transform(chunk, encoding, callback) { this.push(chunk.toString()); callback(); - } + }, }); const stream = resource.getStream(); stream.pipe(tStream); @@ -626,37 +633,38 @@ test("Resource: getProject", (t) => { t.plan(1); const resource = new Resource({ path: "/my/path/to/resource", - project: {getName: () => "Mock Project"} + project: {getName: () => "Mock Project"} as Project, }); const project = resource.getProject(); - t.is(project.getName(), "Mock Project"); + t.is(project!.getName(), "Mock Project"); }); test("Resource: setProject", (t) => { t.plan(1); const resource = new Resource({ - path: "/my/path/to/resource" + path: "/my/path/to/resource", }); - const project = {getName: () => "Mock Project"}; + const project = {getName: () => "Mock Project"} as Project; resource.setProject(project); - t.is(resource.getProject().getName(), "Mock Project"); + t.is(resource.getProject()!.getName(), "Mock Project"); }); test("Resource: reassign with setProject", (t) => { t.plan(2); const resource = new Resource({ path: "/my/path/to/resource", - project: {getName: () => "Mock Project"} + project: {getName: () => "Mock Project"} as Project, }); - const project = {getName: () => "New Mock Project"}; + const project = {getName: () => "New Mock Project"} as Project; const error = t.throws(() => resource.setProject(project)); t.is(error.message, "Unable to assign project New Mock Project to resource /my/path/to/resource: " + - "Resource is already associated to project " + project); + "Resource is already associated to project Mock Project"); }); test("Resource: constructor with stream", async (t) => { const stream = new Stream.Readable(); - stream._read = function() {}; + // eslint-disable-next-line @typescript-eslint/no-empty-function + stream._read = function () {}; stream.push("I am a "); stream.push("readable "); stream.push("stream!"); @@ -665,7 +673,7 @@ test("Resource: constructor with stream", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", stream, - sourceMetadata: {} // Needs to be passed in order to get the "modified" state + sourceMetadata: {}, // Needs to be passed in order to get the "modified" state }); t.is(resource.getSourceMetadata().contentModified, false); @@ -686,7 +694,7 @@ test("integration stat - resource size", async (t) => { statInfo, createStream: () => { return createReadStream(fsPath); - } + }, }); t.is(await resource.getSize(), 91); diff --git a/test/lib/ResourceFacade.js b/test/lib/ResourceFacade.ts similarity index 75% rename from test/lib/ResourceFacade.js rename to test/lib/ResourceFacade.ts index 5dee2fc8..ebf4e274 100644 --- a/test/lib/ResourceFacade.js +++ b/test/lib/ResourceFacade.ts @@ -1,20 +1,20 @@ import test from "ava"; import sinon from "sinon"; -import Resource from "../../lib/Resource.js"; -import ResourceFacade from "../../lib/ResourceFacade.js"; +import Resource, {type ResourceInterface} from "../../src/Resource.js"; +import ResourceFacade from "../../src/ResourceFacade.js"; -test.afterEach.always( (t) => { +test.afterEach.always(() => { sinon.restore(); }); test("Create instance", (t) => { const resource = new Resource({ path: "/my/path/to/resource", - string: "my content" + string: "my content", }); const resourceFacade = new ResourceFacade({ path: "/my/path", - resource + resource, }); t.is(resourceFacade.getPath(), "/my/path", "Returns correct path"); t.is(resourceFacade.getName(), "path", "Returns correct name"); @@ -23,31 +23,33 @@ test("Create instance", (t) => { test("Create instance: Missing parameters", (t) => { t.throws(() => { + // @ts-expect-error testing missing arguments new ResourceFacade({ path: "/my/path", }); }, { instanceOf: Error, - message: "Unable to create ResourceFacade: Missing parameter 'resource'" + message: "Unable to create ResourceFacade: Missing parameter 'resource'", }); t.throws(() => { new ResourceFacade({ + // @ts-expect-error testing missing arguments resource: {}, }); }, { instanceOf: Error, - message: "Unable to create ResourceFacade: Missing parameter 'path'" + message: "Unable to create ResourceFacade: Missing parameter 'path'", }); }); test("ResourceFacade #clone", async (t) => { const resource = new Resource({ path: "/my/path/to/resource", - string: "my content" + string: "my content", }); const resourceFacade = new ResourceFacade({ path: "/my/path", - resource + resource, }); const clone = await resourceFacade.clone(); @@ -58,11 +60,11 @@ test("ResourceFacade #clone", async (t) => { test("ResourceFacade #setPath", (t) => { const resource = new Resource({ path: "/my/path/to/resource", - string: "my content" + string: "my content", }); const resourceFacade = new ResourceFacade({ path: "/my/path", - resource + resource, }); const err = t.throws(() => { @@ -72,25 +74,27 @@ test("ResourceFacade #setPath", (t) => { }); test("ResourceFacade provides same public functions as Resource", (t) => { - const resource = new Resource({ + const resource: ResourceInterface = new Resource({ path: "/my/path/to/resource", - string: "my content" + string: "my content", }); const resourceFacade = new ResourceFacade({ path: "/my/path", - resource + resource, }); const methods = Object.getOwnPropertyNames(Resource.prototype) - .filter((p) => (!p.startsWith("_") && typeof resource[p] === "function")); + .filter((p) => (!p.startsWith("_") && typeof resource[p as keyof typeof resource] === "function")); methods.forEach((method) => { - t.truthy(resourceFacade[method], `resourceFacade provides function #${method}`); + t.truthy(method in resourceFacade, `resourceFacade provides function #${method}`); if (["constructor", "getPath", "getName", "setPath", "clone"].includes(method)) { // special functions with separate tests return; } + // @ts-expect-error Stubbing the resource const stub = sinon.stub(resource, method); + // @ts-expect-error Checking stubbed resource resourceFacade[method]("argument"); t.is(stub.callCount, 1, `Resource#${method} stub got called once by resourceFacade#${method}`); stub.restore(); diff --git a/test/lib/ResourceTagCollection.js b/test/lib/ResourceTagCollection.ts similarity index 82% rename from test/lib/ResourceTagCollection.js rename to test/lib/ResourceTagCollection.ts index 1329187c..cf1d8093 100644 --- a/test/lib/ResourceTagCollection.js +++ b/test/lib/ResourceTagCollection.ts @@ -1,18 +1,18 @@ import test from "ava"; import sinon from "sinon"; -import Resource from "../../lib/Resource.js"; -import ResourceTagCollection from "../../lib/ResourceTagCollection.js"; +import Resource from "../../src/Resource.js"; +import ResourceTagCollection from "../../src/ResourceTagCollection.js"; -test.afterEach.always((t) => { +test.afterEach.always(() => { sinon.restore(); }); test("setTag", (t) => { const resource = new Resource({ - path: "/some/path" + path: "/some/path", }); const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); const validateResourceSpy = sinon.spy(tagCollection, "_getPath"); @@ -23,8 +23,8 @@ test("setTag", (t) => { t.deepEqual(tagCollection._pathTags, { "/some/path": { - "abc:MyTag": "my value" - } + "abc:MyTag": "my value", + }, }, "Tag correctly stored"); t.is(validateResourceSpy.callCount, 1, "_getPath called once"); @@ -42,26 +42,26 @@ test("setTag", (t) => { test("setTag: Value defaults to true", (t) => { const resource = new Resource({ - path: "/some/path" + path: "/some/path", }); const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); tagCollection.setTag(resource, "abc:MyTag"); t.deepEqual(tagCollection._pathTags, { "/some/path": { - "abc:MyTag": true - } + "abc:MyTag": true, + }, }, "Tag correctly stored"); }); test("getTag", (t) => { const resource = new Resource({ - path: "/some/path" + path: "/some/path", }); const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); tagCollection.setTag(resource, "abc:MyTag", 123); @@ -83,15 +83,15 @@ test("getTag", (t) => { test("getTag with prefilled tags", (t) => { const resource = new Resource({ - path: "/some/path" + path: "/some/path", }); const tagCollection = new ResourceTagCollection({ allowedTags: ["abc:MyTag"], tags: { "/some/path": { - "abc:MyTag": 123 - } - } + "abc:MyTag": 123, + }, + }, }); const validateResourceSpy = sinon.spy(tagCollection, "_getPath"); @@ -112,10 +112,10 @@ test("getTag with prefilled tags", (t) => { test("clearTag", (t) => { const resource = new Resource({ - path: "/some/path" + path: "/some/path", }); const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); tagCollection.setTag(resource, "abc:MyTag", 123); @@ -127,8 +127,8 @@ test("clearTag", (t) => { t.deepEqual(tagCollection._pathTags, { "/some/path": { - "abc:MyTag": undefined - } + "abc:MyTag": undefined, + }, }, "Tag value set to undefined"); t.is(validateResourceSpy.callCount, 1, "_getPath called once"); @@ -142,130 +142,130 @@ test("clearTag", (t) => { test("_validateTag: Not in list of allowed tags", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); t.throws(() => { tagCollection._validateTag("abc:MyOtherTag"); }, { instanceOf: Error, message: `Tag "abc:MyOtherTag" not accepted by this collection. ` + - `Accepted tags are: abc:MyTag. Accepted namespaces are: *none*` + `Accepted tags are: abc:MyTag. Accepted namespaces are: *none*`, }); }); test("_validateTag: Empty list of tags and namespaces", (t) => { const tagCollection = new ResourceTagCollection({ allowedTags: [], - allowedNamespaces: [] + allowedNamespaces: [], }); t.throws(() => { tagCollection._validateTag("abc:MyOtherTag"); }, { instanceOf: Error, message: `Tag "abc:MyOtherTag" not accepted by this collection. ` + - `Accepted tags are: *none*. Accepted namespaces are: *none*` + `Accepted tags are: *none*. Accepted namespaces are: *none*`, }); }); test("_validateTag: Missing colon", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["aBcMyTag"] + allowedTags: ["aBcMyTag"], }); t.throws(() => { tagCollection._validateTag("aBcMyTag"); }, { instanceOf: Error, - message: `Invalid Tag "aBcMyTag": Colon required after namespace` + message: `Invalid Tag "aBcMyTag": Colon required after namespace`, }); }); test("_validateTag: Too many colons", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["aBc:My:Tag"] + allowedTags: ["aBc:My:Tag"], }); t.throws(() => { tagCollection._validateTag("aBc:My:Tag"); }, { instanceOf: Error, - message: `Invalid Tag "aBc:My:Tag": Expected exactly one colon but found 2` + message: `Invalid Tag "aBc:My:Tag": Expected exactly one colon but found 2`, }); }); test("_validateTag: Invalid namespace with uppercase letter", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["aBc:MyTag"] + allowedTags: ["aBc:MyTag"], }); t.throws(() => { tagCollection._validateTag("aBc:MyTag"); }, { instanceOf: Error, - message: `Invalid Tag "aBc:MyTag": Namespace part must be alphanumeric, lowercase and start with a letter` + message: `Invalid Tag "aBc:MyTag": Namespace part must be alphanumeric, lowercase and start with a letter`, }); }); test("_validateTag: Invalid namespace starting with number", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["0abc:MyTag"] + allowedTags: ["0abc:MyTag"], }); t.throws(() => { tagCollection._validateTag("0abc:MyTag"); }, { instanceOf: Error, - message: `Invalid Tag "0abc:MyTag": Namespace part must be alphanumeric, lowercase and start with a letter` + message: `Invalid Tag "0abc:MyTag": Namespace part must be alphanumeric, lowercase and start with a letter`, }); }); test("_validateTag: Invalid namespace containing an illegal character", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["a🦦c:MyTag"] + allowedTags: ["a🦦c:MyTag"], }); t.throws(() => { tagCollection._validateTag("a🦦c:MyTag"); }, { instanceOf: Error, - message: `Invalid Tag "a🦦c:MyTag": Namespace part must be alphanumeric, lowercase and start with a letter` + message: `Invalid Tag "a🦦c:MyTag": Namespace part must be alphanumeric, lowercase and start with a letter`, }); }); test("_validateTag: Invalid tag name starting with number", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:0MyTag"] + allowedTags: ["abc:0MyTag"], }); t.throws(() => { tagCollection._validateTag("abc:0MyTag"); }, { instanceOf: Error, - message: `Invalid Tag "abc:0MyTag": Name part must be alphanumeric and start with a capital letter` + message: `Invalid Tag "abc:0MyTag": Name part must be alphanumeric and start with a capital letter`, }); }); test("_validateTag: Invalid tag name starting with lowercase letter", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:myTag"] + allowedTags: ["abc:myTag"], }); t.throws(() => { tagCollection._validateTag("abc:myTag"); }, { instanceOf: Error, - message: `Invalid Tag "abc:myTag": Name part must be alphanumeric and start with a capital letter` + message: `Invalid Tag "abc:myTag": Name part must be alphanumeric and start with a capital letter`, }); }); test("_validateTag: Invalid tag name containing an illegal character", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:My/Tag"] + allowedTags: ["abc:My/Tag"], }); t.throws(() => { tagCollection._validateTag("abc:My/Tag"); }, { instanceOf: Error, - message: `Invalid Tag "abc:My/Tag": Name part must be alphanumeric and start with a capital letter` + message: `Invalid Tag "abc:My/Tag": Name part must be alphanumeric and start with a capital letter`, }); }); test("_validateValue: Valid values", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); tagCollection._validateValue("bla"); tagCollection._validateValue(""); @@ -279,50 +279,54 @@ test("_validateValue: Valid values", (t) => { test("_validateValue: Invalid value of type object", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); t.throws(() => { + // @ts-expect-error testing invalid value tagCollection._validateValue({foo: "bar"}); }, { instanceOf: Error, - message: "Invalid Tag Value: Must be of type string, number or boolean but is object" + message: "Invalid Tag Value: Must be of type string, number or boolean but is object", }); }); test("_validateValue: Invalid value undefined", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); t.throws(() => { + // @ts-expect-error testing invalid value tagCollection._validateValue(undefined); }, { instanceOf: Error, - message: "Invalid Tag Value: Must be of type string, number or boolean but is undefined" + message: "Invalid Tag Value: Must be of type string, number or boolean but is undefined", }); }); test("_validateValue: Invalid value null", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); t.throws(() => { + // @ts-expect-error testing invalid value tagCollection._validateValue(null); }, { instanceOf: Error, - message: "Invalid Tag Value: Must be of type string, number or boolean but is object" + message: "Invalid Tag Value: Must be of type string, number or boolean but is object", }); }); test("_getPath: Empty path", (t) => { const tagCollection = new ResourceTagCollection({ - allowedTags: ["abc:MyTag"] + allowedTags: ["abc:MyTag"], }); t.throws(() => { + // @ts-expect-error testing invalid value tagCollection._getPath({ - getPath: () => "" + getPath: () => "", }); }, { instanceOf: Error, - message: "Invalid Resource: Resource path must not be empty" + message: "Invalid Resource: Resource path must not be empty", }); }); diff --git a/test/lib/WriterCollection.js b/test/lib/WriterCollection.ts similarity index 87% rename from test/lib/WriterCollection.js rename to test/lib/WriterCollection.ts index 7e389452..ee6b50fe 100644 --- a/test/lib/WriterCollection.js +++ b/test/lib/WriterCollection.ts @@ -1,7 +1,7 @@ import test from "ava"; import sinon from "sinon"; -import WriterCollection from "../../lib/WriterCollection.js"; -import Resource from "../../lib/Resource.js"; +import WriterCollection from "../../src/WriterCollection.js"; +import Resource from "../../src/Resource.js"; test("Constructor: Path mapping regex", (t) => { const myWriter = {}; @@ -11,7 +11,7 @@ test("Constructor: Path mapping regex", (t) => { "/": myWriter, "/my/path/": myWriter, "/my/": myWriter, - } + }, }); t.is(writer._basePathRegex.toString(), "^((?:/)??(?:/my/)??(?:/my/path/)??)+.*?$", "Created correct path mapping regular expression"); @@ -25,7 +25,7 @@ test("Constructor: Path mapping regex has correct escaping", (t) => { "/My\\Weird.Path/": myWriter, "/my/pa$h/": myWriter, "/my/": myWriter, - } + }, }); t.is(writer._basePathRegex.toString(), "^((?:/My\\\\Weird\\.Path/)??(?:/my/)??(?:/my/pa\\$h/)??)+.*?$", "Created correct path mapping regular expression"); @@ -34,7 +34,7 @@ test("Constructor: Path mapping regex has correct escaping", (t) => { test("Constructor: Throws for missing path mapping", (t) => { const err = t.throws(() => { new WriterCollection({ - name: "myCollection" + name: "myCollection", }); }); t.is(err.message, "Cannot create WriterCollection myCollection: Missing parameter 'writerMapping'", @@ -45,7 +45,7 @@ test("Constructor: Throws for empty path mapping", (t) => { const err = t.throws(() => { new WriterCollection({ name: "myCollection", - writerMapping: {} + writerMapping: {}, }); }); t.is(err.message, "Cannot create WriterCollection myCollection: Empty parameter 'writerMapping'", @@ -54,14 +54,14 @@ test("Constructor: Throws for empty path mapping", (t) => { test("Constructor: Throws for empty path", (t) => { const myWriter = { - _write: sinon.stub() + _write: sinon.stub(), }; const err = t.throws(() => { new WriterCollection({ name: "myCollection", writerMapping: { - "": myWriter - } + "": myWriter, + }, }); }); t.is(err.message, "Empty path in path mapping of WriterCollection myCollection", @@ -70,14 +70,14 @@ test("Constructor: Throws for empty path", (t) => { test("Constructor: Throws for missing leading slash", (t) => { const myWriter = { - _write: sinon.stub() + _write: sinon.stub(), }; const err = t.throws(() => { new WriterCollection({ name: "myCollection", writerMapping: { - "my/path/": myWriter - } + "my/path/": myWriter, + }, }); }); t.is(err.message, "Missing leading slash in path mapping 'my/path/' of WriterCollection myCollection", @@ -86,14 +86,14 @@ test("Constructor: Throws for missing leading slash", (t) => { test("Constructor: Throws for missing trailing slash", (t) => { const myWriter = { - _write: sinon.stub() + _write: sinon.stub(), }; const err = t.throws(() => { new WriterCollection({ name: "myCollection", writerMapping: { - "/my/path": myWriter - } + "/my/path": myWriter, + }, }); }); t.is(err.message, "Missing trailing slash in path mapping '/my/path' of WriterCollection myCollection", @@ -102,34 +102,34 @@ test("Constructor: Throws for missing trailing slash", (t) => { test("Write", async (t) => { const myPathWriter = { - _write: sinon.stub() + _write: sinon.stub(), }; const myWriter = { - _write: sinon.stub() + _write: sinon.stub(), }; const generalWriter = { - _write: sinon.stub() + _write: sinon.stub(), }; const writerCollection = new WriterCollection({ name: "myCollection", writerMapping: { "/my/path/": myPathWriter, "/my/": myWriter, - "/": generalWriter - } + "/": generalWriter, + }, }); const myPathResource = new Resource({ path: "/my/path/resource.res", - string: "content" + string: "content", }); const myResource = new Resource({ path: "/my/resource.res", - string: "content" + string: "content", }); const resource = new Resource({ path: "/resource.res", - string: "content" + string: "content", }); await writerCollection.write(myPathResource, "options 1"); @@ -150,21 +150,21 @@ test("Write", async (t) => { test("byGlob", async (t) => { const myPathWriter = { - _byGlob: sinon.stub().resolves([]) + _byGlob: sinon.stub().resolves([]), }; const myWriter = { - _byGlob: sinon.stub().resolves([]) + _byGlob: sinon.stub().resolves([]), }; const generalWriter = { - _byGlob: sinon.stub().resolves([]) + _byGlob: sinon.stub().resolves([]), }; const writerCollection = new WriterCollection({ name: "myCollection", writerMapping: { "/my/path/": myPathWriter, "/my/": myWriter, - "/": generalWriter - } + "/": generalWriter, + }, }); await writerCollection.byGlob("/**"); @@ -180,21 +180,21 @@ test("byGlob", async (t) => { test("byPath", async (t) => { const myPathWriter = { - _byPath: sinon.stub().resolves(null) + _byPath: sinon.stub().resolves(null), }; const myWriter = { - _byPath: sinon.stub().resolves(null) + _byPath: sinon.stub().resolves(null), }; const generalWriter = { - _byPath: sinon.stub().resolves(null) + _byPath: sinon.stub().resolves(null), }; const writerCollection = new WriterCollection({ name: "myCollection", writerMapping: { "/my/path/": myPathWriter, "/my/": myWriter, - "/": generalWriter - } + "/": generalWriter, + }, }); await writerCollection.byPath("/my/resource.res"); diff --git a/test/lib/adapters/AbstractAdapter.js b/test/lib/adapters/AbstractAdapter.ts similarity index 81% rename from test/lib/adapters/AbstractAdapter.js rename to test/lib/adapters/AbstractAdapter.ts index 16a06708..e4758e08 100644 --- a/test/lib/adapters/AbstractAdapter.js +++ b/test/lib/adapters/AbstractAdapter.ts @@ -1,45 +1,47 @@ import test from "ava"; -import AbstractAdapter from "../../../lib/adapters/AbstractAdapter.js"; -import {createResource} from "../../../lib/resourceFactory.js"; +import AbstractAdapter from "../../../src/adapters/AbstractAdapter.js"; +import {createResource} from "../../../src/resourceFactory.js"; +import {type LegacyResource} from "../../../src/Resource.js"; +import {type Project} from "@ui5/project/specifications/Project"; class MyAbstractAdapter extends AbstractAdapter { } test("Missing paramter: virBasePath", (t) => { t.throws(() => { - new MyAbstractAdapter({}); + new MyAbstractAdapter({} as {virBasePath: string; excludes?: string[]; project?: Project}); }, { - message: "Unable to create adapter: Missing parameter 'virBasePath'" + message: "Unable to create adapter: Missing parameter 'virBasePath'", }, "Threw with expected error message"); }); test("virBasePath must be absolute", (t) => { t.throws(() => { new MyAbstractAdapter({ - virBasePath: "foo" + virBasePath: "foo", }); }, { - message: "Unable to create adapter: Virtual base path must be absolute but is 'foo'" + message: "Unable to create adapter: Virtual base path must be absolute but is 'foo'", }, "Threw with expected error message"); }); test("virBasePath must end with a slash", (t) => { t.throws(() => { new MyAbstractAdapter({ - virBasePath: "/foo" + virBasePath: "/foo", }); }, { - message: "Unable to create adapter: Virtual base path must end with a slash but is '/foo'" + message: "Unable to create adapter: Virtual base path must end with a slash but is '/foo'", }, "Threw with expected error message"); }); test("_migrateResource", async (t) => { // Any JS object which might be a kind of resource - const resource = { - _path: "/test.js" + const resource: LegacyResource = { + _path: "/test.js", }; const writer = new MyAbstractAdapter({ - virBasePath: "/" + virBasePath: "/", }); const migratedResource = await writer._migrateResource(resource); @@ -52,16 +54,16 @@ test("_assignProjectToResource: Resource is already assigned to another project path: "/test.js", project: { getName: () => "test.lib", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); const writer = new MyAbstractAdapter({ virBasePath: "/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); const error = t.throws(() => writer._assignProjectToResource(resource)); @@ -74,8 +76,8 @@ test("_isPathHandled", (t) => { virBasePath: "/dest2/writer/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.true(writer._isPathHandled("/dest2/writer/test.js"), "Returned expected result"); @@ -90,8 +92,8 @@ test("_resolveVirtualPathToBase (read mode)", (t) => { virBasePath: "/dest2/writer/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.is(writer._resolveVirtualPathToBase("/dest2/writer/test.js"), "test.js", "Returned expected path"); @@ -106,8 +108,8 @@ test("_resolveVirtualPathToBase (read mode): Path does not starting with path co virBasePath: "/dest2/writer/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.is(writer._resolveVirtualPathToBase("/dest2/tmp/test.js"), null, "Returned null"); @@ -121,13 +123,13 @@ test("_resolveVirtualPathToBase (read mode): Path Must be absolute", (t) => { virBasePath: "/dest2/writer/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.throws(() => writer._resolveVirtualPathToBase("./dest2/write"), { message: - `Failed to resolve virtual path './dest2/write': Path must be absolute` + `Failed to resolve virtual path './dest2/write': Path must be absolute`, }, "Threw with expected error message"); }); @@ -136,8 +138,8 @@ test("_resolveVirtualPathToBase (write mode)", (t) => { virBasePath: "/dest2/writer/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.is(writer._resolveVirtualPathToBase("/dest2/writer/test.js", true), "test.js", "Returned expected path"); @@ -153,32 +155,32 @@ test("_resolveVirtualPathToBase (write mode): Path does not starting with path c virBasePath: "/dest2/writer/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.throws(() => writer._resolveVirtualPathToBase("/dest2/tmp/test.js", true), { message: `Failed to write resource with virtual path '/dest2/tmp/test.js': ` + - `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'` + `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'`, }, "Threw with expected error message"); t.throws(() => writer._resolveVirtualPathToBase("/dest2/writer/../reader", true), { message: `Failed to write resource with virtual path '/dest2/writer/../reader': ` + - `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'` + `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'`, }, "Threw with expected error message"); t.throws(() => writer._resolveVirtualPathToBase("/dest2/write", true), { message: `Failed to write resource with virtual path '/dest2/write': ` + - `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'` + `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'`, }, "Threw with expected error message"); t.throws(() => writer._resolveVirtualPathToBase("/..//write", true), { message: `Failed to write resource with virtual path '/..//write': ` + - `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'` + `Path must start with the configured virtual base path of the adapter. Base path: '/dest2/writer/'`, }, "Threw with expected error message"); }); @@ -187,54 +189,54 @@ test("_resolveVirtualPathToBase (write mode): Path Must be absolute", (t) => { virBasePath: "/dest2/writer/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.throws(() => writer._resolveVirtualPathToBase("./dest2/write", true), { message: - `Failed to resolve virtual path './dest2/write': Path must be absolute` + `Failed to resolve virtual path './dest2/write': Path must be absolute`, }, "Threw with expected error message"); }); -test("_normalizePattern", async (t) => { +test("_normalizePattern", (t) => { const writer = new MyAbstractAdapter({ virBasePath: "/path/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); - t.deepEqual(await writer._normalizePattern("/*/{src,test}/**"), [ + t.deepEqual(writer._normalizePattern("/*/{src,test}/**"), [ "src/**", - "test/**" + "test/**", ], "Returned expected patterns"); }); -test("_normalizePattern: Match base directory", async (t) => { +test("_normalizePattern: Match base directory", (t) => { const writer = new MyAbstractAdapter({ virBasePath: "/path/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); - t.deepEqual(await writer._normalizePattern("/*"), [""], + t.deepEqual(writer._normalizePattern("/*"), [""], "Returned an empty pattern since the input pattern matches the base directory only"); }); -test("_normalizePattern: Match sub-directory", async (t) => { +test("_normalizePattern: Match sub-directory", (t) => { const writer = new MyAbstractAdapter({ virBasePath: "/path/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); - t.deepEqual(await writer._normalizePattern("/path/*"), ["*"], + t.deepEqual(writer._normalizePattern("/path/*"), ["*"], "Returned expected patterns"); }); @@ -243,8 +245,8 @@ test("_normalizePattern: Match all", (t) => { virBasePath: "/path/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.deepEqual(writer._normalizePattern("/**/*"), ["**/*"], @@ -256,8 +258,8 @@ test("_normalizePattern: Relative path segment above virtual root directory", (t virBasePath: "/path/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.deepEqual(writer._normalizePattern("/path/../../*"), [], @@ -269,8 +271,8 @@ test("_normalizePattern: Relative path segment resolving to base directory", (t) virBasePath: "/path/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.deepEqual(writer._normalizePattern("/*/../*"), [""], @@ -282,8 +284,8 @@ test("_normalizePattern: Relative path segment", (t) => { virBasePath: "/path/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.deepEqual(writer._normalizePattern("/path/../*"), [""], @@ -295,8 +297,8 @@ test("_normalizePattern: Relative path segment within base directory, matching a virBasePath: "/path/", project: { getName: () => "test.lib1", - getVersion: () => "2.0.0" - } + getVersion: () => "2.0.0", + } as Project, }); t.deepEqual(writer._normalizePattern("/path/path2/../**/*"), ["**/*"], diff --git a/test/lib/adapters/FileSystem.js b/test/lib/adapters/FileSystem.ts similarity index 72% rename from test/lib/adapters/FileSystem.js rename to test/lib/adapters/FileSystem.ts index 9e41965b..fa45bdca 100644 --- a/test/lib/adapters/FileSystem.js +++ b/test/lib/adapters/FileSystem.ts @@ -1,13 +1,13 @@ import test from "ava"; -import FileSystem from "../../../lib/adapters/FileSystem.js"; +import FileSystem from "../../../src/adapters/FileSystem.js"; test.serial("Missing parameter: fsBasePath", (t) => { t.throws(() => { new FileSystem({ - virBasePath: "/" + virBasePath: "/", }); }, { - message: "Unable to create adapter: Missing parameter 'fsBasePath'" + message: "Unable to create adapter: Missing parameter 'fsBasePath'", }, "Threw with expected error message"); }); diff --git a/test/lib/adapters/FileSystem_read.js b/test/lib/adapters/FileSystem_read.ts similarity index 91% rename from test/lib/adapters/FileSystem_read.js rename to test/lib/adapters/FileSystem_read.ts index f5bee36f..e7f6c754 100644 --- a/test/lib/adapters/FileSystem_read.js +++ b/test/lib/adapters/FileSystem_read.ts @@ -2,12 +2,12 @@ import test from "ava"; import sinon from "sinon"; import esmock from "esmock"; import path from "node:path"; -import {createAdapter} from "../../../lib/resourceFactory.js"; +import {createAdapter} from "../../../src/resourceFactory.js"; test("glob resources from application.a w/ virtual base path prefix", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const resources = await readerWriter.byGlob("/app/**/*.html"); @@ -17,7 +17,7 @@ test("glob resources from application.a w/ virtual base path prefix", async (t) test("glob resources from application.a w/o virtual base path prefix", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const resources = await readerWriter.byGlob("/**/*.html"); @@ -27,7 +27,7 @@ test("glob resources from application.a w/o virtual base path prefix", async (t) test("glob virtual directory w/o virtual base path prefix", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const resources = await readerWriter.byGlob("/*", {nodir: false}); @@ -37,12 +37,12 @@ test("glob virtual directory w/o virtual base path prefix", async (t) => { test("glob virtual directory w/o virtual base path prefix and multiple patterns", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const resources = await readerWriter.byGlob([ "/*", - "/*" + "/*", ], {nodir: false}); t.is(resources.length, 1, "Found exactly one resource"); }); @@ -50,7 +50,7 @@ test("glob virtual directory w/o virtual base path prefix and multiple patterns" test("glob virtual directory w/ virtual base path prefix", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/one/two/" + virBasePath: "/app/one/two/", }); const resources = await readerWriter.byGlob("/app/*", {nodir: false}); @@ -60,7 +60,7 @@ test("glob virtual directory w/ virtual base path prefix", async (t) => { test("glob virtual directory w/o virtual base path prefix and nodir: true", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const resources = await readerWriter.byGlob("/*", {nodir: true}); @@ -70,12 +70,12 @@ test("glob virtual directory w/o virtual base path prefix and nodir: true", asyn test("glob virtual directory w/o virtual base path prefix and nodir: true and multiple patterns", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const resources = await readerWriter.byGlob([ "/*", - "/*" + "/*", ], {nodir: true}); t.is(resources.length, 0, "Found no resources"); }); @@ -83,7 +83,7 @@ test("glob virtual directory w/o virtual base path prefix and nodir: true and mu test("glob virtual directory w/ virtual base path prefix and nodir: true", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/one/two/" + virBasePath: "/app/one/two/", }); const resources = await readerWriter.byGlob("/app/*", {nodir: true}); @@ -93,10 +93,10 @@ test("glob virtual directory w/ virtual base path prefix and nodir: true", async test("glob resources from application.a with directory exclude", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); - await readerWriter.byGlob("/!(pony,unicorn)/**").then(function(resources) { + await readerWriter.byGlob("/!(pony,unicorn)/**").then(function (resources) { t.is(resources.length, 2, "Found exactly two resource"); }); }); @@ -104,7 +104,7 @@ test("glob resources from application.a with directory exclude", async (t) => { test("glob all above virtual base path (path traversal)", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const res = await readerWriter.byGlob([ @@ -116,7 +116,7 @@ test("glob all above virtual base path (path traversal)", async (t) => { test("glob virtual directory above virtual base path (path traversal)", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const res = await readerWriter.byGlob([ @@ -134,7 +134,7 @@ test("glob not existing directory with existing file path (nodir: true)", async // globby will throw if it is attempted to glob the content of a directory // which is actually a file await t.throwsAsync(srcReader.byGlob("/test/library/l/Test.html/*", {nodir: true}), { - message: /ENOTDIR/ + message: /ENOTDIR/, }, "Threw with expected error message"); }); @@ -147,14 +147,14 @@ test("glob not existing directory with existing file path (nodir: false)", async // globby will throw if it is attempted to glob the content of a directory // which is actually a file await t.throwsAsync(srcReader.byGlob("/test/library/l/Test.html/*", {nodir: false}), { - message: /ENOTDIR/ + message: /ENOTDIR/, }, "Threw with expected error message"); }); test("byPath", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); const resource = await readerWriter.byPath("/resources/app/index.html", {nodir: true}); @@ -164,7 +164,7 @@ test("byPath", async (t) => { test("byPath virtual directory above base path (path traversal)", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); const resource = await readerWriter.byPath("/resources/app/../package.json", {nodir: true}); @@ -174,7 +174,7 @@ test("byPath virtual directory above base path (path traversal)", async (t) => { test("byPath: Root dir w/ trailing slash", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); const resource = await readerWriter.byPath("/resources/app/", {nodir: false}); @@ -184,7 +184,7 @@ test("byPath: Root dir w/ trailing slash", async (t) => { test("byPath: Root dir w/o trailing slash", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); const resource = await readerWriter.byPath("/resources/app", {nodir: false}); @@ -194,7 +194,7 @@ test("byPath: Root dir w/o trailing slash", async (t) => { test("byPath: Virtual directory w/ trailing slash", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); const resource = await readerWriter.byPath("/resources/", {nodir: false}); @@ -204,7 +204,7 @@ test("byPath: Virtual directory w/ trailing slash", async (t) => { test("byPath: Virtual directory w/o trailing slash", async (t) => { const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); const resource = await readerWriter.byPath("/resources", {nodir: false}); @@ -216,7 +216,7 @@ test("byPath: Incorrect virtual directory w/o trailing slash", async (t) => { // tested elsewhere const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); // TODO: This should actually not match @@ -240,13 +240,13 @@ test("static excludes: glob library src and test", async (t) => { const srcReader = createAdapter({ fsBasePath: "./test/fixtures/library.l/src", virBasePath: "/resources/", - excludes + excludes, }); const testReader = createAdapter({ fsBasePath: "./test/fixtures/library.l/test", virBasePath: "/test-resources/", - excludes + excludes, }); const srcResources = await srcReader.byGlob("/**/*", {nodir: true}); @@ -254,7 +254,7 @@ test("static excludes: glob library src and test", async (t) => { t.is(srcResources.length, 1, "Found one src resource"); t.deepEqual(srcResources.map(getPathFromResource), [ - "/resources/library/l/.library" + "/resources/library/l/.library", ], "Found expected src resources"); t.is(testResources.length, 0, "Found no test resources"); @@ -271,13 +271,13 @@ test("static excludes: glob library src and test with double negation", async (t const srcReader = createAdapter({ fsBasePath: "./test/fixtures/library.l/src", virBasePath: "/resources/", - excludes + excludes, }); const testReader = createAdapter({ fsBasePath: "./test/fixtures/library.l/test", virBasePath: "/test-resources/", - excludes + excludes, }); const srcResources = await srcReader.byGlob("/**/*", {nodir: true}); @@ -285,12 +285,12 @@ test("static excludes: glob library src and test with double negation", async (t t.is(srcResources.length, 1, "Found one src resource"); t.deepEqual(srcResources.map(getPathFromResource), [ - "/resources/library/l/.library" + "/resources/library/l/.library", ], "Found expected src resources"); t.is(testResources.length, 1, "Found one test resource"); t.deepEqual(testResources.map(getPathFromResource), [ - "/test-resources/library/l/Test2.html" + "/test-resources/library/l/Test2.html", ], "Found expected test resources"); }); @@ -305,14 +305,14 @@ test("static excludes: glob library test with double negation", async (t) => { const testReader = createAdapter({ fsBasePath: "./test/fixtures/library.l/test", virBasePath: "/test-resources/", - excludes + excludes, }); const testResources = await testReader.byGlob("/**/*", {nodir: true}); t.is(testResources.length, 1, "Found one test resource"); t.deepEqual(testResources.map(getPathFromResource), [ - "/test-resources/library/l/Test2.html" + "/test-resources/library/l/Test2.html", ], "Found expected test resources"); }); @@ -321,8 +321,8 @@ test("static excludes: glob with virtual root exclude", async (t) => { fsBasePath: "./test/fixtures/library.l/src", virBasePath: "/resources/", excludes: [ - "/**" - ] + "/**", + ], }); const resources = await srcReader.byGlob("/**/*", {nodir: true}); @@ -335,8 +335,8 @@ test("static excludes: glob with negated directory exclude, excluding resources" fsBasePath: "./test/fixtures/library.l/src", virBasePath: "/resources/", excludes: [ - "/!({pony,unicorn})/**" - ] + "/!({pony,unicorn})/**", + ], }); const resources = await srcReader.byGlob("/**/*", {nodir: true}); @@ -349,8 +349,8 @@ test("static excludes: glob with negated directory exclude, not excluding resour fsBasePath: "./test/fixtures/library.l/src", virBasePath: "/resources/", excludes: [ - "/!(resources)/**" - ] + "/!(resources)/**", + ], }); const resources = await srcReader.byGlob("/**/*", {nodir: true}); @@ -362,7 +362,7 @@ test("static excludes: byPath exclude everything in sub-directory", async (t) => const readerWriter = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", virBasePath: "/resources/app/", - excludes: ["/resources/app/**"] + excludes: ["/resources/app/**"], }); const resource = await readerWriter.byPath("/resources/app/index.html", {nodir: true}); @@ -375,8 +375,8 @@ test("static excludes: byPath exclude with negation", async (t) => { virBasePath: "/resources/app/", excludes: [ "/resources/app/**", - "!/resources/app/index.html" - ] + "!/resources/app/index.html", + ], }); const resource = await readerWriter.byPath("/resources/app/index.html", {nodir: true}); @@ -389,8 +389,8 @@ test("static excludes: byPath exclude with unused negation", async (t) => { virBasePath: "/resources/app/", excludes: [ "!/resources/app/index.html", - "/resources/app/**" - ] + "/resources/app/**", + ], }); const resource = await readerWriter.byPath("/resources/app/index.html", {nodir: true}); @@ -402,8 +402,8 @@ test("static excludes: byPath exclude with negated directory pattern, excluding fsBasePath: "./test/fixtures/application.a/webapp", virBasePath: "/resources/app/", excludes: [ - "/!({pony,unicorn})/**" - ] + "/!({pony,unicorn})/**", + ], }); const resource = await readerWriter.byPath("/resources/app/index.html", {nodir: true}); @@ -415,8 +415,8 @@ test("static excludes: byPath exclude with negated directory pattern, not exclud fsBasePath: "./test/fixtures/application.a/webapp", virBasePath: "/resources/app/", excludes: [ - "/!(resources)/**" - ] + "/!(resources)/**", + ], }); const resource = await readerWriter.byPath("/resources/app/index.html", {nodir: true}); @@ -430,14 +430,14 @@ test("byPath: exclude with unused negation", async (t) => { excludes: [ "!/resources/app/i18n/**", "/resources/app/**", - "!/resources/app/manifest.json" - ] + "!/resources/app/manifest.json", + ], }); const [manifest, i18n, i18ni18n] = await Promise.all([ readerWriter.byPath("/resources/app/manifest.json", {nodir: true}), readerWriter.byPath("/resources/app/i18n.properties", {nodir: true}), - readerWriter.byPath("/resources/app/i18n/i18n.properties", {nodir: true}) + readerWriter.byPath("/resources/app/i18n/i18n.properties", {nodir: true}), ]); t.truthy(manifest, "Found manifest.json resource"); t.is(i18n, null, "i18n resource is excluded"); @@ -455,13 +455,13 @@ test("static excludes: glob library src and test with double negation (nodir: fa const srcReader = createAdapter({ fsBasePath: "./test/fixtures/library.l/src", virBasePath: "/resources/", - excludes + excludes, }); const testReader = createAdapter({ fsBasePath: "./test/fixtures/library.l/test", virBasePath: "/test-resources/", - excludes + excludes, }); const srcResources = await srcReader.byGlob("/**/*", {nodir: false}); @@ -471,7 +471,7 @@ test("static excludes: glob library src and test with double negation (nodir: fa t.is(testResources.length, 1, "Found one test resource"); t.deepEqual(testResources.map(getPathFromResource), [ - "/test-resources/library/l/Test2.html" + "/test-resources/library/l/Test2.html", ], "Found expected test resources"); }); @@ -480,8 +480,8 @@ test("static excludes: glob with virtual root exclude (nodir: false)", async (t) fsBasePath: "./test/fixtures/library.l/src", virBasePath: "/resources/", excludes: [ - "/**" - ] + "/**", + ], }); const resources = await srcReader.byGlob("/**/*", {nodir: false}); @@ -493,8 +493,8 @@ test("static excludes: glob with negated directory exclude, excluding resources fsBasePath: "./test/fixtures/library.l/src", virBasePath: "/resources/", excludes: [ - "/!({pony,unicorn})/**" - ] + "/!({pony,unicorn})/**", + ], }); const resources = await srcReader.byGlob("/**/*", {nodir: false}); @@ -507,8 +507,8 @@ test("static excludes: glob with negated directory exclude, not excluding resour fsBasePath: "./test/fixtures/library.l/src", virBasePath: "/resources/", excludes: [ - "/!(resources)/**" - ] + "/!(resources)/**", + ], }); const resources = await srcReader.byGlob("/**/*", {nodir: false}); @@ -521,8 +521,8 @@ test("static excludes: glob with exclude (nodir: false)", async (t) => { fsBasePath: "./test/fixtures/library.l/", virBasePath: "/", excludes: [ - "/test/**" - ] + "/test/**", + ], }); const resources = await srcReader.byGlob("/test/library/l/Test.html", {nodir: false}); @@ -534,7 +534,7 @@ test("glob with useGitignore: true", async (t) => { const srcReader = createAdapter({ fsBasePath: "./test/fixtures/library.l/", virBasePath: "/", - useGitignore: true + useGitignore: true, }); const resources = await srcReader.byGlob("/**/*"); @@ -553,17 +553,17 @@ test("byPath with useGitignore: true", async (t) => { const {isGitIgnored} = await import("globby"); const isGitIgnoredSpy = sinon.stub().callsFake(isGitIgnored); - const FileSystem = await esmock("../../../lib/adapters/FileSystem.js", { - "globby": { - isGitIgnored: isGitIgnoredSpy - } + const FileSystem = await esmock("../../../src/adapters/FileSystem.js", { + globby: { + isGitIgnored: isGitIgnoredSpy, + }, }); const fsBasePath = "./test/fixtures/library.l/"; const srcReader = new FileSystem({ fsBasePath, virBasePath: "/", - useGitignore: true + useGitignore: true, }); const testResource = await srcReader.byPath("/test/library/l/Test.html"); diff --git a/test/lib/adapters/FileSystem_write.js b/test/lib/adapters/FileSystem_write.ts similarity index 98% rename from test/lib/adapters/FileSystem_write.js rename to test/lib/adapters/FileSystem_write.ts index 8386c2a5..c3f7eabe 100644 --- a/test/lib/adapters/FileSystem_write.js +++ b/test/lib/adapters/FileSystem_write.ts @@ -6,7 +6,7 @@ import test from "ava"; import {rimraf} from "rimraf"; import sinon from "sinon"; -import {createAdapter, createResource} from "../../../lib/resourceFactory.js"; +import {createAdapter, createResource} from "../../../src/resourceFactory.js"; function getFileContent(path) { return readFile(path, "utf8"); @@ -33,12 +33,12 @@ test.beforeEach(async (t) => { t.context.readerWriters = { source: createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }), dest: createAdapter({ fsBasePath: "./test/tmp/adapters/FileSystemWrite/" + tmpDirName, - virBasePath: "/app/" - }) + virBasePath: "/app/", + }), }; }); @@ -127,7 +127,7 @@ test("Write with readOnly and drain options set should fail", async (t) => { // Write resource content to another readerWriter await t.throwsAsync(readerWriters.dest.write(resource, {readOnly: true, drain: true}), { message: "Error while writing resource /app/index.html: " + - "Do not use options 'drain' and 'readOnly' at the same time." + "Do not use options 'drain' and 'readOnly' at the same time.", }); }); diff --git a/test/lib/adapters/FileSystem_write_large_file.js b/test/lib/adapters/FileSystem_write_large_file.ts similarity index 89% rename from test/lib/adapters/FileSystem_write_large_file.js rename to test/lib/adapters/FileSystem_write_large_file.ts index bb8fbfb6..cea40f69 100644 --- a/test/lib/adapters/FileSystem_write_large_file.js +++ b/test/lib/adapters/FileSystem_write_large_file.ts @@ -3,8 +3,8 @@ import {fileURLToPath} from "node:url"; import {Buffer} from "node:buffer"; import {readFile} from "node:fs/promises"; -import FileSystem from "../../../lib/adapters/FileSystem.js"; -import Resource from "../../../lib/Resource.js"; +import FileSystem from "../../../src/adapters/FileSystem.js"; +import Resource from "../../../src/Resource.js"; import path from "node:path"; test.serial("Stream a large file from source to target", async (t) => { @@ -16,14 +16,14 @@ test.serial("Stream a large file from source to target", async (t) => { const fileSystem = new FileSystem({ fsBasePath, - virBasePath: "/" + virBasePath: "/", }); const largeBuffer = Buffer.alloc(1048576); // 1MB await fileSystem.write(new Resource({ path: "/large-file.txt", - buffer: largeBuffer + buffer: largeBuffer, })); t.deepEqual(await readFile(path.join(fsBasePath, "large-file.txt")), largeBuffer, diff --git a/test/lib/adapters/Memory_read.js b/test/lib/adapters/Memory_read.ts similarity index 91% rename from test/lib/adapters/Memory_read.js rename to test/lib/adapters/Memory_read.ts index 70a034b2..9e468cbe 100644 --- a/test/lib/adapters/Memory_read.js +++ b/test/lib/adapters/Memory_read.ts @@ -1,5 +1,5 @@ import test from "ava"; -import {createAdapter, createResource} from "../../../lib/resourceFactory.js"; +import {createAdapter, createResource} from "../../../src/resourceFactory.js"; async function fillFromFs(readerWriter, {fsBasePath = "./test/fixtures/glob", virBasePath = "/app/"} = {}) { const fsReader = createAdapter({ @@ -8,7 +8,7 @@ async function fillFromFs(readerWriter, {fsBasePath = "./test/fixtures/glob", vi }); const fsResources = await fsReader.byGlob("/**/*"); - return Promise.all(fsResources.map(function(resource) { + return Promise.all(fsResources.map(function (resource) { return readerWriter.write(resource); })); } @@ -20,6 +20,7 @@ function matchGlobResult(t, resources, expectedResources) { return resource.getPath(); }); + // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < expectedResources.length; i++) { const expectedResource = expectedResources[i]; t.true( @@ -31,12 +32,12 @@ function matchGlobResult(t, resources, expectedResources) { test("glob resources from application.a w/ virtual base path prefix", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); const resource = createResource({ path: "/app/index.html", - string: "test" + string: "test", }); await readerWriter.write(resource); @@ -46,12 +47,12 @@ test("glob resources from application.a w/ virtual base path prefix", async (t) test("glob resources from application.a w/o virtual base path prefix", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); const resource = createResource({ path: "/app/index.html", - string: "test" + string: "test", }); await readerWriter.write(resource); @@ -62,7 +63,7 @@ test("glob resources from application.a w/o virtual base path prefix", async (t) test("glob virtual directory w/o virtual base path prefix", async (t) => { // TODO: Add similar test (globbing on empty directory) for FS RL const readerWriter = createAdapter({ - virBasePath: "/app/one/two/" + virBasePath: "/app/one/two/", }); const resources = await readerWriter.byGlob("/*", {nodir: false}); @@ -72,12 +73,12 @@ test("glob virtual directory w/o virtual base path prefix", async (t) => { test("glob virtual directory w/o virtual base path prefix and multiple patterns", async (t) => { // TODO: Add similar test (globbing on empty directory) for FS RL const readerWriter = createAdapter({ - virBasePath: "/app/one/two/" + virBasePath: "/app/one/two/", }); const resources = await readerWriter.byGlob([ "/*", - "/*" + "/*", ], {nodir: false}); t.is(resources.length, 1, "Found exactly one resource"); }); @@ -85,7 +86,7 @@ test("glob virtual directory w/o virtual base path prefix and multiple patterns" test("glob virtual directory w/ virtual base path prefix", async (t) => { // TODO: Add similar test (globbing on empty directory) for FS RL const readerWriter = createAdapter({ - virBasePath: "/app/one/two/" + virBasePath: "/app/one/two/", }); const resources = await readerWriter.byGlob("/app/*", {nodir: false}); @@ -94,7 +95,7 @@ test("glob virtual directory w/ virtual base path prefix", async (t) => { test("glob virtual directory w/o virtual base path prefix and nodir: true", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/one/two/" + virBasePath: "/app/one/two/", }); const resources = await readerWriter.byGlob("/*", {nodir: true}); @@ -103,7 +104,7 @@ test("glob virtual directory w/o virtual base path prefix and nodir: true", asyn test("glob virtual directory w/ virtual base path prefix and nodir: true", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/one/two/" + virBasePath: "/app/one/two/", }); const resources = await readerWriter.byGlob("/app/*", {nodir: true}); @@ -112,12 +113,12 @@ test("glob virtual directory w/ virtual base path prefix and nodir: true", async test("glob virtual directory w/ virtual base path prefix and nodir: true and multiple patterns", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/one/two/" + virBasePath: "/app/one/two/", }); const resources = await readerWriter.byGlob([ "/*", - "/*" + "/*", ], {nodir: true}); t.is(resources.length, 0, "Found no resources"); }); @@ -125,7 +126,7 @@ test("glob virtual directory w/ virtual base path prefix and nodir: true and mul /* Load more data from FS into memory */ test("glob all", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); const resources = await readerWriter.byGlob("/**/*.*"); @@ -135,7 +136,7 @@ test("glob all", async (t) => { test("glob all from root", async (t) => { t.plan(2); const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); const resources = await readerWriter.byGlob("/*/*.*"); @@ -144,7 +145,7 @@ test("glob all from root", async (t) => { test("glob all with virtual path included", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); @@ -155,7 +156,7 @@ test("glob all with virtual path included", async (t) => { test("glob a specific filetype (yaml)", async (t) => { t.plan(2); const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); @@ -168,7 +169,7 @@ test("glob a specific filetype (yaml)", async (t) => { test("glob two specific filetype (yaml and js)", async (t) => { t.plan(4); const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); @@ -176,7 +177,7 @@ test("glob two specific filetype (yaml and js)", async (t) => { const expectedFiles = [ "/app/application.b/ui5.yaml", "/app/application.a/ui5.yaml", - "/app/application.a/webapp/test.js" + "/app/application.a/webapp/test.js", ]; matchGlobResult(t, resources, expectedFiles); }); @@ -184,17 +185,17 @@ test("glob two specific filetype (yaml and js)", async (t) => { test("glob a specific filetype (json) with exclude pattern", async (t) => { t.plan(3); const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); const resources = await readerWriter.byGlob([ "/**/*.json", - "!/**/*package.json" + "!/**/*package.json", ]); const expectedFiles = [ "/app/application.b/webapp/manifest.json", - "/app/application.b/webapp/embedded/manifest.json" + "/app/application.b/webapp/embedded/manifest.json", ]; matchGlobResult(t, resources, expectedFiles); }); @@ -202,14 +203,14 @@ test("glob a specific filetype (json) with exclude pattern", async (t) => { test("glob a specific filetype (json) with multiple exclude pattern", async (t) => { t.plan(2); const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); const resources = await readerWriter.byGlob([ "/**/*.json", "!/**/*package.json", - "!/**/embedded/manifest.json" + "!/**/embedded/manifest.json", ]); matchGlobResult(t, resources, ["/app/application.b/webapp/manifest.json"]); }); @@ -217,7 +218,7 @@ test("glob a specific filetype (json) with multiple exclude pattern", async (t) test("glob (normalized) root directory (=> fs root)", async (t) => { t.plan(2); const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); @@ -233,7 +234,7 @@ test("glob (normalized) root directory (=> fs root)", async (t) => { test("glob root directory", async (t) => { t.plan(2); const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); @@ -244,7 +245,7 @@ test("glob root directory", async (t) => { test("glob subdirectory", async (t) => { t.plan(3); const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter); @@ -259,7 +260,7 @@ test("glob subdirectory", async (t) => { test("glob all resources above virtual base path (path traversal)", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.a/webapp", @@ -274,7 +275,7 @@ test("glob all resources above virtual base path (path traversal)", async (t) => test("glob virtual directory above virtual base path (path traversal)", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.a/webapp", @@ -289,7 +290,7 @@ test("glob virtual directory above virtual base path (path traversal)", async (t test("byPath", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.a/webapp", @@ -302,7 +303,7 @@ test("byPath", async (t) => { test("byPath virtual directory above base path (path traversal)", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.a/webapp", @@ -313,7 +314,6 @@ test("byPath virtual directory above base path (path traversal)", async (t) => { t.is(resource, null, "Found no resource"); }); - function getPathFromResource(resource) { return resource.getPath(); } @@ -329,7 +329,7 @@ test("static excludes: glob library src and test", async (t) => { ]; const srcReader = createAdapter({ virBasePath: "/resources/", - excludes + excludes, }); await fillFromFs(srcReader, { fsBasePath: "./test/fixtures/library.l/src", @@ -338,7 +338,7 @@ test("static excludes: glob library src and test", async (t) => { const testReader = createAdapter({ virBasePath: "/test-resources/", - excludes + excludes, }); await fillFromFs(testReader, { fsBasePath: "./test/fixtures/library.l/test", @@ -350,7 +350,7 @@ test("static excludes: glob library src and test", async (t) => { t.is(srcResources.length, 1, "Found one src resource"); t.deepEqual(srcResources.map(getPathFromResource), [ - "/resources/library/l/.library" + "/resources/library/l/.library", ], "Found expected src resources"); t.is(testResources.length, 0, "Found no test resources"); @@ -366,7 +366,7 @@ test("static excludes: glob library src and test with double negation", async (t ]; const srcReader = createAdapter({ virBasePath: "/resources/", - excludes + excludes, }); await fillFromFs(srcReader, { fsBasePath: "./test/fixtures/library.l/src", @@ -375,11 +375,11 @@ test("static excludes: glob library src and test with double negation", async (t const testReader = createAdapter({ virBasePath: "/test-resources/", - excludes + excludes, }); await fillFromFs(testReader, { fsBasePath: "./test/fixtures/library.l/test", - virBasePath: "/test-resources/" + virBasePath: "/test-resources/", }); const srcResources = await srcReader.byGlob("/**/*", {nodir: true}); @@ -387,12 +387,12 @@ test("static excludes: glob library src and test with double negation", async (t t.is(srcResources.length, 1, "Found one src resource"); t.deepEqual(srcResources.map(getPathFromResource), [ - "/resources/library/l/.library" + "/resources/library/l/.library", ], "Found expected src resources"); t.is(testResources.length, 1, "Found one test resource"); t.deepEqual(testResources.map(getPathFromResource), [ - "/test-resources/library/l/Test2.html" + "/test-resources/library/l/Test2.html", ], "Found expected test resources"); }); @@ -406,18 +406,18 @@ test("static excludes: glob library test with double negation", async (t) => { const testReader = createAdapter({ virBasePath: "/test-resources/", - excludes + excludes, }); await fillFromFs(testReader, { fsBasePath: "./test/fixtures/library.l/test", - virBasePath: "/test-resources/" + virBasePath: "/test-resources/", }); const testResources = await testReader.byGlob("/**/*", {nodir: true}); t.is(testResources.length, 1, "Found one test resource"); t.deepEqual(testResources.map(getPathFromResource), [ - "/test-resources/library/l/Test2.html" + "/test-resources/library/l/Test2.html", ], "Found expected test resources"); }); @@ -425,8 +425,8 @@ test("static excludes: glob with virtual root exclude", async (t) => { const srcReader = createAdapter({ virBasePath: "/resources/", excludes: [ - "/**" - ] + "/**", + ], }); await fillFromFs(srcReader, { fsBasePath: "./test/fixtures/library.l/src", @@ -442,8 +442,8 @@ test("static excludes: glob with negated directory exclude, excluding resources" const srcReader = createAdapter({ virBasePath: "/resources/", excludes: [ - "/!({pony,unicorn})/**" - ] + "/!({pony,unicorn})/**", + ], }); await fillFromFs(srcReader, { fsBasePath: "./test/fixtures/library.l/src", @@ -459,8 +459,8 @@ test("static excludes: glob with negated directory exclude, not excluding resour const srcReader = createAdapter({ virBasePath: "/resources/", excludes: [ - "/!(resources)/**" - ] + "/!(resources)/**", + ], }); await fillFromFs(srcReader, { fsBasePath: "./test/fixtures/library.l/src", @@ -475,7 +475,7 @@ test("static excludes: glob with negated directory exclude, not excluding resour test("static excludes: byPath exclude everything in sub-directory", async (t) => { const readerWriter = createAdapter({ virBasePath: "/resources/app/", - excludes: ["/resources/app/**"] + excludes: ["/resources/app/**"], }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.a/webapp", @@ -491,8 +491,8 @@ test("static excludes: byPath exclude with negation", async (t) => { virBasePath: "/resources/app/", excludes: [ "/resources/app/**", - "!/resources/app/index.html" - ] + "!/resources/app/index.html", + ], }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.a/webapp", @@ -508,8 +508,8 @@ test("static excludes: byPath exclude with unused negation", async (t) => { virBasePath: "/resources/app/", excludes: [ "!/resources/app/index.html", - "/resources/app/**" - ] + "/resources/app/**", + ], }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.a/webapp", @@ -524,8 +524,8 @@ test("static excludes: byPath exclude with negated directory pattern, excluding const readerWriter = createAdapter({ virBasePath: "/resources/app/", excludes: [ - "/!({pony,unicorn})/**" - ] + "/!({pony,unicorn})/**", + ], }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.a/webapp", @@ -540,8 +540,8 @@ test("static excludes: byPath exclude with negated directory pattern, not exclud const readerWriter = createAdapter({ virBasePath: "/resources/app/", excludes: [ - "/!(resources)/**" - ] + "/!(resources)/**", + ], }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.a/webapp", @@ -558,8 +558,8 @@ test("byPath: exclude with unused negation", async (t) => { excludes: [ "!/resources/app/i18n/**", "/resources/app/**", - "!/resources/app/manifest.json" - ] + "!/resources/app/manifest.json", + ], }); await fillFromFs(readerWriter, { fsBasePath: "./test/fixtures/application.b/webapp", @@ -569,7 +569,7 @@ test("byPath: exclude with unused negation", async (t) => { const [manifest, i18n, i18ni18n] = await Promise.all([ readerWriter.byPath("/resources/app/manifest.json", {nodir: true}), readerWriter.byPath("/resources/app/i18n.properties", {nodir: true}), - readerWriter.byPath("/resources/app/i18n/i18n.properties", {nodir: true}) + readerWriter.byPath("/resources/app/i18n/i18n.properties", {nodir: true}), ]); t.truthy(manifest, "Found manifest.json resource"); t.is(i18n, null, "i18n resource is excluded"); @@ -586,7 +586,7 @@ test("static excludes: glob library src and test with double negation (nodir: fa ]; const srcReader = createAdapter({ virBasePath: "/resources/", - excludes + excludes, }); await fillFromFs(srcReader, { fsBasePath: "./test/fixtures/library.l/src", @@ -595,7 +595,7 @@ test("static excludes: glob library src and test with double negation (nodir: fa const testReader = createAdapter({ virBasePath: "/test-resources/", - excludes + excludes, }); await fillFromFs(testReader, { fsBasePath: "./test/fixtures/library.l/test", @@ -609,7 +609,7 @@ test("static excludes: glob library src and test with double negation (nodir: fa t.is(testResources.length, 1, "Found one test resource"); t.deepEqual(testResources.map(getPathFromResource), [ - "/test-resources/library/l/Test2.html" + "/test-resources/library/l/Test2.html", ], "Found expected test resources"); }); @@ -617,8 +617,8 @@ test("static excludes: glob with virtual root exclude (nodir: false)", async (t) const srcReader = createAdapter({ virBasePath: "/resources/", excludes: [ - "/**" - ] + "/**", + ], }); await fillFromFs(srcReader, { fsBasePath: "./test/fixtures/library.l/src", @@ -633,8 +633,8 @@ test("static excludes: glob with negated directory exclude, excluding resources const srcReader = createAdapter({ virBasePath: "/resources/", excludes: [ - "/!({pony,unicorn})/**" - ] + "/!({pony,unicorn})/**", + ], }); await fillFromFs(srcReader, { fsBasePath: "./test/fixtures/library.l/src", @@ -650,8 +650,8 @@ test("static excludes: glob with negated directory exclude, not excluding resour const srcReader = createAdapter({ virBasePath: "/resources/", excludes: [ - "/!(resources)/**" - ] + "/!(resources)/**", + ], }); await fillFromFs(srcReader, { fsBasePath: "./test/fixtures/library.l/src", @@ -666,7 +666,7 @@ test("static excludes: glob with negated directory exclude, not excluding resour test("byPath returns new resource", async (t) => { const originalResource = createResource({ path: "/app/index.html", - string: "test" + string: "test", }); const memoryAdapter = createAdapter({virBasePath: "/"}); @@ -694,7 +694,7 @@ test("byPath returns new resource", async (t) => { test("byGlob returns new resources", async (t) => { const originalResource = createResource({ path: "/app/index.html", - string: "test" + string: "test", }); const memoryAdapter = createAdapter({virBasePath: "/"}); diff --git a/test/lib/adapters/Memory_write.js b/test/lib/adapters/Memory_write.ts similarity index 85% rename from test/lib/adapters/Memory_write.js rename to test/lib/adapters/Memory_write.ts index d1cbad8a..437eddb8 100644 --- a/test/lib/adapters/Memory_write.js +++ b/test/lib/adapters/Memory_write.ts @@ -1,14 +1,14 @@ import test from "ava"; -import {createAdapter, createResource} from "../../../lib/resourceFactory.js"; +import {createAdapter, createResource} from "../../../src/resourceFactory.js"; import sinon from "sinon"; test("glob resources from application.a w/ virtual base path prefix", async (t) => { const dest = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); const res = createResource({ - path: "/app/index.html" + path: "/app/index.html", }); await dest.write(res); const resources = await dest.byGlob("/app/*.html"); @@ -18,11 +18,11 @@ test("glob resources from application.a w/ virtual base path prefix", async (t) test("glob resources from application.a w/o virtual base path prefix", async (t) => { const dest = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); const res = createResource({ - path: "/app/index.html" + path: "/app/index.html", }); await dest.write(res); const resources = await dest.byGlob("/**/*.html"); @@ -31,16 +31,16 @@ test("glob resources from application.a w/o virtual base path prefix", async (t) test("Write resource w/ virtual base path", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/" + virBasePath: "/app/", }); const res = createResource({ - path: "/app/test.html" + path: "/app/test.html", }); await readerWriter.write(res); t.deepEqual(readerWriter._virFiles, { - "test.html": res + "test.html": res, }, "Adapter added resource with correct path"); t.deepEqual(Object.keys(readerWriter._virDirs), [], "Adapter added correct virtual directories"); @@ -49,22 +49,22 @@ test("Write resource w/ virtual base path", async (t) => { test("Write resource w/o virtual base path", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/" + virBasePath: "/", }); const res = createResource({ - path: "/one/two/three/test.html" + path: "/one/two/three/test.html", }); await readerWriter.write(res); t.deepEqual(readerWriter._virFiles, { - "one/two/three/test.html": res + "one/two/three/test.html": res, }, "Adapter added resource with correct path"); t.deepEqual(Object.keys(readerWriter._virDirs), [ "one/two/three", "one/two", - "one" + "one", ], "Adapter added correct virtual directories"); const dirRes = readerWriter._virDirs["one/two/three"]; @@ -74,22 +74,22 @@ test("Write resource w/o virtual base path", async (t) => { test("Write resource w/ deep virtual base path", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/a/" + virBasePath: "/app/a/", }); const res = createResource({ - path: "/app/a/one/two/three/test.html" + path: "/app/a/one/two/three/test.html", }); await readerWriter.write(res); t.deepEqual(readerWriter._virFiles, { - "one/two/three/test.html": res + "one/two/three/test.html": res, }, "Adapter added resource with correct path"); t.deepEqual(Object.keys(readerWriter._virDirs), [ "one/two/three", "one/two", - "one" + "one", ], "Adapter added correct virtual directories"); const dirRes = readerWriter._virDirs["one/two/three"]; @@ -99,32 +99,32 @@ test("Write resource w/ deep virtual base path", async (t) => { test("Write resource w/ crazy virtual base path", async (t) => { const readerWriter = createAdapter({ - virBasePath: "/app/🐛/" + virBasePath: "/app/🐛/", }); const res = createResource({ - path: "/app/🐛/one\\/2/3️⃣/test" + path: "/app/🐛/one\\/2/3️⃣/test", }); await readerWriter.write(res); t.deepEqual(readerWriter._virFiles, { - "one\\/2/3️⃣/test": res + "one\\/2/3️⃣/test": res, }, "Adapter added resource with correct path"); t.deepEqual(Object.keys(readerWriter._virDirs), [ "one\\/2/3️⃣", "one\\/2", - "one\\" + "one\\", ], "Adapter added correct virtual directories"); }); test("Migration of resource is executed", async (t) => { const writer = createAdapter({ - virBasePath: "/" + virBasePath: "/", }); const resource = createResource({ - path: "/test.js" + path: "/test.js", }); const migrateResourceWriterSpy = sinon.spy(writer, "_migrateResource"); @@ -134,12 +134,12 @@ test("Migration of resource is executed", async (t) => { test("Resource: Change instance after write", async (t) => { const writer = createAdapter({ - virBasePath: "/" + virBasePath: "/", }); const resource = createResource({ path: "/test.js", - string: "MyInitialContent" + string: "MyInitialContent", }); await writer.write(resource); diff --git a/test/lib/fsInterface.js b/test/lib/fsInterface.ts similarity index 88% rename from test/lib/fsInterface.js rename to test/lib/fsInterface.ts index 4d00222b..788ece66 100644 --- a/test/lib/fsInterface.js +++ b/test/lib/fsInterface.ts @@ -6,10 +6,10 @@ import {fileURLToPath} from "node:url"; import fsSync from "node:fs"; const stat = promisify(fsSync.stat); import {readFile} from "node:fs/promises"; -import fsInterface from "../../lib/fsInterface.js"; -import MemAdapter from "../../lib/adapters/Memory.js"; -import FsAdapter from "../../lib/adapters/FileSystem.js"; -import Resource from "../../lib/Resource.js"; +import fsInterface from "../../src/fsInterface.js"; +import MemAdapter from "../../src/adapters/Memory.js"; +import FsAdapter from "../../src/adapters/FileSystem.js"; +import Resource from "../../src/Resource.js"; const assertReadFile = async (t, readFile, basepath, filepath, content) => { content = content || "content of " + filepath; @@ -39,7 +39,7 @@ function getPath() { test("MemAdapter: readFile", async (t) => { const memAdapter = new MemAdapter({ - virBasePath: "/" + virBasePath: "/", }); const fs = fsInterface(memAdapter); const readFile = promisify(fs.readFile); @@ -47,16 +47,16 @@ test("MemAdapter: readFile", async (t) => { const fsPath = path.join("/", "foo.txt"); await memAdapter.write(new Resource({ path: "/foo.txt", - string: `content of ${fsPath}` + string: `content of ${fsPath}`, })); - `content of ${fsPath}`; + // `content of ${fsPath}`; await assertReadFile(t, readFile, "", fsPath); }); test("FsAdapter: readFile with non-ASCII characters in path", async (t) => { const fsAdapter = new FsAdapter({ virBasePath: "/", - fsBasePath: getPath() + fsBasePath: getPath(), }); const fs = fsInterface(fsAdapter); const readFile = promisify(fs.readFile); @@ -84,7 +84,7 @@ const assertStat = async (t, stat, basepath, filepath) => { test("MemAdapter: stat", async (t) => { const memAdapter = new MemAdapter({ - virBasePath: "/" + virBasePath: "/", }); const fs = fsInterface(memAdapter); const stat = promisify(fs.stat); @@ -92,7 +92,7 @@ test("MemAdapter: stat", async (t) => { const fsPath = path.join("/", "foo.txt"); await memAdapter.write(new Resource({ path: "/foo.txt", - string: `content of ${fsPath}` + string: `content of ${fsPath}`, })); await assertStat(t, stat, "", fsPath); }); @@ -100,7 +100,7 @@ test("MemAdapter: stat", async (t) => { test("FsAdapter: stat", async (t) => { const fsAdapter = new FsAdapter({ virBasePath: "/", - fsBasePath: getPath() + fsBasePath: getPath(), }); const fs = fsInterface(fsAdapter); const stat = promisify(fs.stat); @@ -114,7 +114,7 @@ test("fs: stat", async (t) => { test("MemAdapter: mkdir", async (t) => { const memAdapter = new MemAdapter({ - virBasePath: "/" + virBasePath: "/", }); const fs = fsInterface(memAdapter); const mkdir = promisify(fs.mkdir); diff --git a/test/lib/glob.js b/test/lib/glob.ts similarity index 95% rename from test/lib/glob.js rename to test/lib/glob.ts index 4e8c1b61..40ebdadb 100644 --- a/test/lib/glob.js +++ b/test/lib/glob.ts @@ -1,13 +1,13 @@ import test from "ava"; -import FsAdapter from "../../lib/adapters/FileSystem.js"; +import FsAdapter from "../../src/adapters/FileSystem.js"; // Create readerWriter before running tests test.beforeEach((t) => { t.context.readerWriter = { filesystem: new FsAdapter({ fsBasePath: "./test/fixtures/glob", - virBasePath: "/test-resources/" - }) + virBasePath: "/test-resources/", + }), }; }); @@ -18,6 +18,7 @@ function matchGlobResult(t, resources, expectedResources) { return resource.getPath(); }); + // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < expectedResources.length; i++) { const expectedResource = expectedResources[i]; t.true( @@ -55,7 +56,7 @@ test("glob with virtual base path partially matching", async (t) => { const adapter = new FsAdapter({ fsBasePath: "./test/fixtures/glob/application.a", - virBasePath: "/test-resources/application/a/" + virBasePath: "/test-resources/application/a/", }); const resources = await adapter.byGlob("/test-resources/**/*.*"); @@ -106,7 +107,7 @@ test("glob with multiple patterns", async (t) => { "/test-resources/application.b/webapp/i18n/i18n_de.properties", "/test-resources/application.b/webapp/embedded/i18n/i18n_de.properties", "/test-resources/application.b/ui5.yaml", - "/test-resources/application.a/ui5.yaml" + "/test-resources/application.a/ui5.yaml", ]; matchGlobResult(t, resources, expectedResources); @@ -130,7 +131,7 @@ test("glob two specific filetype (yaml and js)", async (t) => { const expectedResources = [ "/test-resources/application.a/webapp/test.js", "/test-resources/application.b/ui5.yaml", - "/test-resources/application.a/ui5.yaml" + "/test-resources/application.a/ui5.yaml", ]; matchGlobResult(t, resources, expectedResources); @@ -141,7 +142,7 @@ test("glob only a specific filetype (json) with exclude pattern", async (t) => { const resources = await t.context.readerWriter.filesystem.byGlob([ "/**/*.json", - "!/**/*package.json" + "!/**/*package.json", ]); resources.forEach((res) => { @@ -155,7 +156,7 @@ test("glob only a specific filetype (json) with multiple exclude pattern", async const resources = await t.context.readerWriter.filesystem.byGlob([ "/**/*.json", "!/**/*package.json", - "!/**/embedded/manifest.json" + "!/**/embedded/manifest.json", ]); matchGlobResult(t, resources, ["/test-resources/application.b/webapp/manifest.json"]); @@ -203,14 +204,14 @@ test("glob with multiple patterns with static exclude", async (t) => { virBasePath: "/test-resources/", excludes: [ "/test-resources/application.b/**", - "!/test-resources/application.b/**/manifest.json" - ] + "!/test-resources/application.b/**/manifest.json", + ], }).byGlob(["/**/*.yaml", "/test-resources/**/i18n_de.properties"]); const expectedResources = [ "/test-resources/application.a/ui5.yaml", "/test-resources/application.b/webapp/manifest.json", - "/test-resources/application.b/webapp/embedded/manifest.json" + "/test-resources/application.b/webapp/embedded/manifest.json", ]; matchGlobResult(t, resources, expectedResources); }); @@ -403,4 +404,3 @@ test("glob with multiple patterns with static exclude", async (t) => { // const matches = await glob([""], opt); // t.deepEqual(matches.length, 23, "Resources should match"); // }); - diff --git a/test/lib/package-exports.js b/test/lib/package-exports.ts similarity index 71% rename from test/lib/package-exports.js rename to test/lib/package-exports.ts index 13201c3d..3214c140 100644 --- a/test/lib/package-exports.js +++ b/test/lib/package-exports.ts @@ -19,60 +19,60 @@ test("check number of exports", (t) => { [ { exportedSpecifier: "@ui5/fs/adapters/AbstractAdapter", - mappedModule: "../../lib/adapters/AbstractAdapter.js" + mappedModule: "../../src/adapters/AbstractAdapter.js", }, { exportedSpecifier: "@ui5/fs/adapters/FileSystem", - mappedModule: "../../lib/adapters/FileSystem.js" + mappedModule: "../../src/adapters/FileSystem.js", }, { exportedSpecifier: "@ui5/fs/adapters/Memory", - mappedModule: "../../lib/adapters/Memory.js" + mappedModule: "../../src/adapters/Memory.js", }, { exportedSpecifier: "@ui5/fs/AbstractReader", - mappedModule: "../../lib/AbstractReader.js" + mappedModule: "../../src/AbstractReader.js", }, { exportedSpecifier: "@ui5/fs/AbstractReaderWriter", - mappedModule: "../../lib/AbstractReaderWriter.js" + mappedModule: "../../src/AbstractReaderWriter.js", }, { exportedSpecifier: "@ui5/fs/DuplexCollection", - mappedModule: "../../lib/DuplexCollection.js" + mappedModule: "../../src/DuplexCollection.js", }, { exportedSpecifier: "@ui5/fs/fsInterface", - mappedModule: "../../lib/fsInterface.js" + mappedModule: "../../src/fsInterface.js", }, { exportedSpecifier: "@ui5/fs/ReaderCollection", - mappedModule: "../../lib/ReaderCollection.js" + mappedModule: "../../src/ReaderCollection.js", }, { exportedSpecifier: "@ui5/fs/ReaderCollectionPrioritized", - mappedModule: "../../lib/ReaderCollectionPrioritized.js" + mappedModule: "../../src/ReaderCollectionPrioritized.js", }, { exportedSpecifier: "@ui5/fs/readers/Filter", - mappedModule: "../../lib/readers/Filter.js" + mappedModule: "../../src/readers/Filter.js", }, { exportedSpecifier: "@ui5/fs/readers/Link", - mappedModule: "../../lib/readers/Link.js" + mappedModule: "../../src/readers/Link.js", }, { exportedSpecifier: "@ui5/fs/Resource", - mappedModule: "../../lib/Resource.js" + mappedModule: "../../src/Resource.js", }, { exportedSpecifier: "@ui5/fs/resourceFactory", - mappedModule: "../../lib/resourceFactory.js" + mappedModule: "../../src/resourceFactory.js", }, // Internal modules (only to be used by @ui5/* packages) { exportedSpecifier: "@ui5/fs/internal/ResourceTagCollection", - mappedModule: "../../lib/ResourceTagCollection.js" + mappedModule: "../../src/ResourceTagCollection.js", }, ].forEach(({exportedSpecifier, mappedModule}) => { test(`${exportedSpecifier}`, async (t) => { diff --git a/test/lib/readers/Filter.js b/test/lib/readers/Filter.ts similarity index 76% rename from test/lib/readers/Filter.js rename to test/lib/readers/Filter.ts index c48264b4..d9bfb8a7 100644 --- a/test/lib/readers/Filter.js +++ b/test/lib/readers/Filter.ts @@ -1,22 +1,22 @@ import test from "ava"; import sinon from "sinon"; -import Filter from "../../../lib/readers/Filter.js"; +import Filter from "../../../src/readers/Filter.js"; test("_byGlob: Basic filter", async (t) => { const abstractReader = { - _byGlob: sinon.stub().returns(Promise.resolve(["resource a", "resource b"])) + _byGlob: sinon.stub().returns(Promise.resolve(["resource a", "resource b"])), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Filter({ reader: abstractReader, - callback: function(resource) { + callback: function (resource) { if (resource === "resource a") { return false; } return true; - } + }, }); const resources = await readerCollection._byGlob("anyPattern", {}, trace); @@ -25,19 +25,19 @@ test("_byGlob: Basic filter", async (t) => { test("_byPath: Negative filter", async (t) => { const abstractReader = { - _byPath: sinon.stub().returns(Promise.resolve("resource a")) + _byPath: sinon.stub().returns(Promise.resolve("resource a")), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Filter({ reader: abstractReader, - callback: function(resource) { + callback: function (resource) { if (resource === "resource a") { return false; } return true; - } + }, }); const resource = await readerCollection._byPath("anyPattern", {}, trace); @@ -46,19 +46,19 @@ test("_byPath: Negative filter", async (t) => { test("_byPath: Positive filter", async (t) => { const abstractReader = { - _byPath: sinon.stub().returns(Promise.resolve("resource b")) + _byPath: sinon.stub().returns(Promise.resolve("resource b")), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Filter({ reader: abstractReader, - callback: function(resource) { + callback: function (resource) { if (resource === "resource a") { return false; } return true; - } + }, }); const resource = await readerCollection._byPath("anyPattern", {}, trace); diff --git a/test/lib/readers/Link.js b/test/lib/readers/Link.ts similarity index 88% rename from test/lib/readers/Link.js rename to test/lib/readers/Link.ts index 01abc5d6..230513bc 100644 --- a/test/lib/readers/Link.js +++ b/test/lib/readers/Link.ts @@ -1,27 +1,27 @@ import test from "ava"; import sinon from "sinon"; -import Link from "../../../lib/readers/Link.js"; -import ResourceFacade from "../../../lib/ResourceFacade.js"; +import Link from "../../../src/readers/Link.js"; +import ResourceFacade from "../../../src/ResourceFacade.js"; test("_byGlob: Basic Link", async (t) => { const dummyResourceA = { - getPath: () => "/resources/some/lib/FileA.js" + getPath: () => "/resources/some/lib/FileA.js", }; const dummyResourceB = { - getPath: () => "/resources/some/lib/dir/FileB.js" + getPath: () => "/resources/some/lib/dir/FileB.js", }; const abstractReader = { - _byGlob: sinon.stub().resolves([dummyResourceA, dummyResourceB]) + _byGlob: sinon.stub().resolves([dummyResourceA, dummyResourceB]), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Link({ reader: abstractReader, pathMapping: { linkPath: `/`, - targetPath: `/resources/some/lib/` - } + targetPath: `/resources/some/lib/`, + }, }); const options = "options"; const resources = await readerCollection._byGlob("anyPattern", options, trace); @@ -46,17 +46,17 @@ test("_byGlob: Basic Link", async (t) => { test("_byGlob: Complex pattern", async (t) => { const abstractReader = { - _byGlob: sinon.stub().resolves([]) + _byGlob: sinon.stub().resolves([]), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Link({ reader: abstractReader, pathMapping: { linkPath: `/`, - targetPath: `/resources/some/lib/` - } + targetPath: `/resources/some/lib/`, + }, }); const options = "options"; @@ -76,17 +76,17 @@ test("_byGlob: Complex pattern", async (t) => { test("_byGlob: Request prefixed with target path", async (t) => { const abstractReader = { - _byGlob: sinon.stub().resolves([]) + _byGlob: sinon.stub().resolves([]), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Link({ reader: abstractReader, pathMapping: { linkPath: `/my/lib/`, - targetPath: `/some/lib/` - } + targetPath: `/some/lib/`, + }, }); const options = "options"; const resources = await readerCollection._byGlob("/some/lib/dir/**", options, trace); @@ -104,17 +104,17 @@ test("_byGlob: Request prefixed with target path", async (t) => { test("_byGlob: Request prefixed with link path", async (t) => { const abstractReader = { - _byGlob: sinon.stub().resolves([]) + _byGlob: sinon.stub().resolves([]), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Link({ reader: abstractReader, pathMapping: { linkPath: `/my/lib/`, - targetPath: `/some/lib/` - } + targetPath: `/some/lib/`, + }, }); const options = "options"; const resources = await readerCollection._byGlob("/my/lib/dir/**", options, trace); @@ -132,20 +132,20 @@ test("_byGlob: Request prefixed with link path", async (t) => { test("_byPath: Basic Link", async (t) => { const dummyResource = { - getPath: () => "/resources/some/lib/dir/File.js" + getPath: () => "/resources/some/lib/dir/File.js", }; const abstractReader = { - _byPath: sinon.stub().resolves(dummyResource) + _byPath: sinon.stub().resolves(dummyResource), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Link({ reader: abstractReader, pathMapping: { linkPath: `/`, - targetPath: `/resources/some/lib/` - } + targetPath: `/resources/some/lib/`, + }, }); const options = "options"; const resource = await readerCollection._byPath("/dir/File.js", options, trace); @@ -165,20 +165,20 @@ test("_byPath: Basic Link", async (t) => { test("_byPath: Rewrite on same level", async (t) => { const dummyResource = { - getPath: () => "/some/lib/dir/File.js" + getPath: () => "/some/lib/dir/File.js", }; const abstractReader = { - _byPath: sinon.stub().resolves(dummyResource) + _byPath: sinon.stub().resolves(dummyResource), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Link({ reader: abstractReader, pathMapping: { linkPath: `/my/lib/`, - targetPath: `/some/lib/` - } + targetPath: `/some/lib/`, + }, }); const options = "options"; const resource = await readerCollection._byPath("/my/lib/dir/File.js", options, trace); @@ -198,17 +198,17 @@ test("_byPath: Rewrite on same level", async (t) => { test("_byPath: No resource found", async (t) => { const abstractReader = { - _byPath: sinon.stub().resolves(null) + _byPath: sinon.stub().resolves(null), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Link({ reader: abstractReader, pathMapping: { linkPath: `/`, - targetPath: `/some/lib/` - } + targetPath: `/some/lib/`, + }, }); const options = "options"; const resource = await readerCollection._byPath("/dir/File.js", options, trace); @@ -226,17 +226,17 @@ test("_byPath: No resource found", async (t) => { test("_byPath: Request different prefix", async (t) => { const abstractReader = { - _byPath: sinon.stub() + _byPath: sinon.stub(), }; const trace = { - collection: sinon.spy() + collection: sinon.spy(), }; const readerCollection = new Link({ reader: abstractReader, pathMapping: { linkPath: `/my/lib/`, - targetPath: `/some/lib/` - } + targetPath: `/some/lib/`, + }, }); const options = "options"; const resource = await readerCollection._byPath("/some/lib/dir/File.js", options, trace); @@ -252,7 +252,7 @@ test("Missing attributes", (t) => { pathMapping: { linkPath: `/`, targetPath: `/`, - } + }, }); }); t.is(err.message, `Missing parameter "reader"`, @@ -260,7 +260,7 @@ test("Missing attributes", (t) => { err = t.throws(() => { new Link({ - reader: abstractReader + reader: abstractReader, }); }); t.is(err.message, `Missing parameter "pathMapping"`, @@ -271,7 +271,7 @@ test("Missing attributes", (t) => { reader: abstractReader, pathMapping: { targetPath: `/`, - } + }, }); }); t.is(err.message, `Path mapping is missing attribute "linkPath"`, @@ -282,7 +282,7 @@ test("Missing attributes", (t) => { reader: abstractReader, pathMapping: { linkPath: `/`, - } + }, }); }); t.is(err.message, `Path mapping is missing attribute "targetPath"`, @@ -294,7 +294,7 @@ test("Missing attributes", (t) => { pathMapping: { linkPath: `/path`, targetPath: `/`, - } + }, }); }); t.is(err.message, `Link path must end with a slash: /path`, @@ -306,7 +306,7 @@ test("Missing attributes", (t) => { pathMapping: { linkPath: `/`, targetPath: `/path`, - } + }, }); }); t.is(err.message, `Target path must end with a slash: /path`, diff --git a/test/lib/resourceFactory.js b/test/lib/resourceFactory.ts similarity index 83% rename from test/lib/resourceFactory.js rename to test/lib/resourceFactory.ts index c535ff90..27c2d0ab 100644 --- a/test/lib/resourceFactory.js +++ b/test/lib/resourceFactory.ts @@ -1,10 +1,10 @@ import test from "ava"; import { createAdapter, createReader, createReaderCollection, createReaderCollectionPrioritized, - createResource, createWriterCollection, createWorkspace, prefixGlobPattern} from "../../lib/resourceFactory.js"; -import FileSystem from "../../lib/adapters/FileSystem.js"; -import Memory from "../../lib/adapters/Memory.js"; -import ReaderCollection from "../../lib/ReaderCollection.js"; + createResource, createWriterCollection, createWorkspace, prefixGlobPattern} from "../../src/resourceFactory.js"; +import FileSystem from "../../src/adapters/FileSystem.js"; +import Memory from "../../src/adapters/Memory.js"; +import ReaderCollection from "../../src/ReaderCollection.js"; import {setLogLevel} from "@ui5/logger"; // Set log level to silly to activate tracing @@ -15,7 +15,7 @@ test("prefixGlobPattern", (t) => { prefixGlobPattern("{/sub-directory-1/,/sub-directory-2/}**", "/pony/path/a"), [ "/pony/path/a/sub-directory-1/**", - "/pony/path/a/sub-directory-2/**" + "/pony/path/a/sub-directory-2/**", ], "GLOBs correctly prefixed"); @@ -50,9 +50,9 @@ test("createAdapter: FS Adapter", async (t) => { fsBasePath: "./test/fixtures/application.a/webapp", virBasePath: "/resources/app/", project: { - getName: () => "my.project" + getName: () => "my.project", }, - excludes: ["**/*.html"] + excludes: ["**/*.html"], }); t.true(adapter instanceof FileSystem, "Returned a FileSystem adapter"); @@ -66,20 +66,20 @@ test("createAdapter: Memory", async (t) => { const adapter = createAdapter({ virBasePath: "/resources/app/", project: { - getName: () => "my.project" + getName: () => "my.project", }, - excludes: ["**/*.html"] + excludes: ["**/*.html"], }); t.true(adapter instanceof Memory, "Returned a Memory adapter"); const resource1 = createResource({ - path: "/resources/app/File.js" + path: "/resources/app/File.js", }); await adapter.write(resource1); const resource2 = createResource({ - path: "/resources/app/index.html" + path: "/resources/app/index.html", }); await adapter.write(resource2); @@ -94,7 +94,7 @@ test("createReader: application project", async (t) => { virBasePath: "/resources/app/", project: { getName: () => "my.project", - getType: () => "application" + getType: () => "application", }, excludes: [ "**/*.html", @@ -102,9 +102,9 @@ test("createReader: application project", async (t) => { "/test/**", "test/**", "!/resources/app/test/**", - "!/test/**/*.html" + "!/test/**/*.html", ], - name: "reader name" + name: "reader name", }); t.true(reader instanceof ReaderCollection, "Returned a ReaderCollection"); @@ -130,16 +130,16 @@ test("createReader: library project", async (t) => { virBasePath: "/resources/lib/", project: { getName: () => "my.project", - getType: () => "library" + getType: () => "library", }, excludes: [ "**/*.html", "/resources/lib/dir/**", "/test-resources/lib/dir/**", "/test/**", - "test/**" + "test/**", ], - name: "reader name" + name: "reader name", }); t.true(reader instanceof ReaderCollection, "Returned a ReaderCollection"); @@ -167,9 +167,9 @@ test("createReader: No project", async (t) => { "/resources/app/dir/**", "/test-resources/app/dir/**", "/test/**", - "test/**" + "test/**", ], - name: "reader name" + name: "reader name", }); t.true(reader instanceof ReaderCollection, "Returned a ReaderCollection"); @@ -184,7 +184,7 @@ test("createReader: No project", async (t) => { "/resources/app/dir/**", "/test-resources/app/dir/**", "/test/**", - "test/**" + "test/**", ], "Excludes do not get prefixed."); }); @@ -192,10 +192,10 @@ test("createReader: Throw error missing 'fsBasePath'", (t) => { const error = t.throws(() => createReader({ virBasePath: "/resources/app/", project: { - getName: () => "my.project" + getName: () => "my.project", }, excludes: ["**/*.html"], - name: "reader name" + name: "reader name", })); t.is(error.message, "Unable to create reader: Missing parameter \"fsBasePath\""); }); @@ -204,22 +204,22 @@ test("createReaderCollection", async (t) => { const adapter = createAdapter({ virBasePath: "/resources/app/", project: { - getName: () => "my.project" + getName: () => "my.project", }, - excludes: ["**/*.html"] + excludes: ["**/*.html"], }); const resource1 = createResource({ - path: "/resources/app/File.js" + path: "/resources/app/File.js", }); const resource2 = createResource({ - path: "/resources/app/index.html" + path: "/resources/app/index.html", }); await adapter.write(resource1); await adapter.write(resource2); const reader = createReaderCollection({ name: "reader name", - readers: [adapter] + readers: [adapter], }); t.true(reader instanceof ReaderCollection, "Returned a ReaderCollection"); t.is(reader._name, "reader name", "Reader has correct name"); @@ -230,26 +230,26 @@ test("createReaderCollection", async (t) => { }); test("createReaderCollectionPrioritized", async (t) => { - const {default: ReaderCollectionPrioritized} = await import("../../lib/ReaderCollectionPrioritized.js"); + const {default: ReaderCollectionPrioritized} = await import("../../src/ReaderCollectionPrioritized.js"); const adapter = createAdapter({ virBasePath: "/resources/app/", project: { - getName: () => "my.project" + getName: () => "my.project", }, - excludes: ["**/*.html"] + excludes: ["**/*.html"], }); const resource1 = createResource({ - path: "/resources/app/File.js" + path: "/resources/app/File.js", }); const resource2 = createResource({ - path: "/resources/app/index.html" + path: "/resources/app/index.html", }); await adapter.write(resource1); await adapter.write(resource2); const reader = createReaderCollectionPrioritized({ name: "reader name", - readers: [adapter] + readers: [adapter], }); t.true(reader instanceof ReaderCollectionPrioritized, "Returned a ReaderCollection"); t.is(reader._name, "reader name", "Reader has correct name"); @@ -260,32 +260,32 @@ test("createReaderCollectionPrioritized", async (t) => { }); test("createWriterCollection", async (t) => { - const {default: WriterCollection} = await import("../../lib/WriterCollection.js"); + const {default: WriterCollection} = await import("../../src/WriterCollection.js"); const adapter1 = createAdapter({ virBasePath: "/", project: { - getName: () => "my.project" - } + getName: () => "my.project", + }, }); const adapter2 = createAdapter({ virBasePath: "/", project: { - getName: () => "my.other.project" - } + getName: () => "my.other.project", + }, }); const resource1 = createResource({ - path: "/resources/app/File.js" + path: "/resources/app/File.js", }); const resource2 = createResource({ - path: "/resources/app2/index.html" + path: "/resources/app2/index.html", }); const writerCollection = createWriterCollection({ name: "writer collection name", writerMapping: { "/resources/app/": adapter1, - "/resources/app2/": adapter2 - } + "/resources/app2/": adapter2, + }, }); t.true(writerCollection instanceof WriterCollection, "Returned a ReaderCollection"); await writerCollection.write(resource1); @@ -300,30 +300,30 @@ test("createWriterCollection", async (t) => { }); test("createWorkspace", async (t) => { - const {default: DuplexCollection} = await import("../../lib/DuplexCollection.js"); + const {default: DuplexCollection} = await import("../../src/DuplexCollection.js"); const reader = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", virBasePath: "/resources/app/", project: { - getName: () => "my.project" - } + getName: () => "my.project", + }, }); const readerWriter = createAdapter({ virBasePath: "/", project: { - getName: () => "my.other.project" - } + getName: () => "my.other.project", + }, }); const writerCollection = createWorkspace({ name: "writer collection name", reader, - writer: readerWriter + writer: readerWriter, }); t.true(writerCollection instanceof DuplexCollection, "Returned a ReaderCollection"); const resource1 = createResource({ - path: "/resources/app/File.js" + path: "/resources/app/File.js", }); await writerCollection.write(resource1); @@ -333,20 +333,19 @@ test("createWorkspace", async (t) => { }); test("createWorkspace: Without writer", async (t) => { - const {default: DuplexCollection} = await import("../../lib/DuplexCollection.js"); - const {default: Memory} = await import("../../lib/adapters/Memory.js"); + const {default: DuplexCollection} = await import("../../src/DuplexCollection.js"); + const {default: Memory} = await import("../../src/adapters/Memory.js"); const reader = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", virBasePath: "/resources/app/", project: { - getName: () => "my.project" - } + getName: () => "my.project", + }, }); const writerCollection = createWorkspace({ name: "writer collection name", - reader + reader, }); t.true(writerCollection instanceof DuplexCollection, "Returned a ReaderCollection"); t.true(writerCollection._writer instanceof Memory, "Internal Writer is created and a MemAdapter"); }); - diff --git a/test/lib/resources.js b/test/lib/resources.ts similarity index 64% rename from test/lib/resources.js rename to test/lib/resources.ts index 64b861c5..35b5ae77 100644 --- a/test/lib/resources.js +++ b/test/lib/resources.ts @@ -2,25 +2,35 @@ import test from "ava"; import sinon from "sinon"; import {readFile} from "node:fs/promises"; +// const test = anyTest as TestFn<{ +// // buildLogger: BuildLogger; +// // logStub: sinon.SinonStub; +// // logHandler: sinon.SinonStub; +// // metadataHandler: sinon.SinonStub; +// // statusHandler: sinon.SinonStub; +// }>; + import {createAdapter, createFilterReader, - createFlatReader, createLinkReader, createResource} from "../../lib/resourceFactory.js"; + createFlatReader, createLinkReader, createResource} from "../../src/resourceFactory.js"; +import {type Project} from "@ui5/project/specifications/Project"; -test.afterEach.always((t) => { +test.afterEach.always(() => { sinon.restore(); }); -function getFileContent(path) { +function getFileContent(path: string) { return readFile(path, "utf8"); } -async function fileEqual(t, actual, expected) { +async function fileEqual(t, actual: string, expected: string) { const actualContent = await getFileContent(actual); const expectedContent = await getFileContent(expected); t.is(actualContent, expectedContent); } ["FileSystem", "Memory"].forEach((adapter) => { - async function getAdapter(config) { + async function getAdapter(config: + {fsBasePath?: string; virBasePath: string; project?: Project; excludes?: string[]; useGitignore?: boolean}) { if (adapter === "Memory") { const fsAdapter = createAdapter(config); const fsResources = await fsAdapter.byGlob("**/*"); @@ -40,22 +50,22 @@ async function fileEqual(t, actual, expected) { Always make sure that every test writes to a separate file! By default, tests are running concurrent. */ test(adapter + - ": Get resource from application.a (/index.html) and write it to /dest/ using a ReadableStream", async (t) => { + ": Get resource from application.a (/index.html) and write it to /dest/ using a ReadableStream", async (t) => { const source = await getAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const dest = await getAdapter({ fsBasePath: "./test/tmp/readerWriters/application.a/simple-read-write", - virBasePath: "/dest/" + virBasePath: "/dest/", }); // Get resource from one readerWriter - const resource = await source.byPath("/app/index.html"); + const resource = await source!.byPath("/app/index.html"); // Write resource content to another readerWriter - resource.setPath("/dest/index_readableStreamTest.html"); - await dest.write(resource); + resource!.setPath("/dest/index_readableStreamTest.html"); + await dest!.write(resource); t.notThrows(async () => { if (adapter === "FileSystem") { @@ -64,8 +74,8 @@ async function fileEqual(t, actual, expected) { "./test/tmp/readerWriters/application.a/simple-read-write/index_readableStreamTest.html", "./test/fixtures/application.a/webapp/index.html"); } else { - const destResource = await dest.byPath("/dest/index_readableStreamTest.html"); - t.deepEqual(await destResource.getString(), await resource.getString()); + const destResource = await dest!.byPath("/dest/index_readableStreamTest.html"); + t.deepEqual(await destResource!.getString(), await resource!.getString()); } }); }); @@ -73,83 +83,83 @@ async function fileEqual(t, actual, expected) { test(adapter + ": Create resource, write and change content", async (t) => { const dest = await getAdapter({ fsBasePath: "./test/tmp/writer/", - virBasePath: "/dest/writer/" + virBasePath: "/dest/writer/", }); const resource = createResource({ path: "/dest/writer/content/test.js", - string: "MyInitialContent" + string: "MyInitialContent", }); - await dest.write(resource); + await dest!.write(resource); resource.setString("MyNewContent"); - const resource1 = await dest.byPath("/dest/writer/content/test.js"); + const resource1 = await dest!.byPath("/dest/writer/content/test.js"); t.is(await resource.getString(), "MyNewContent"); - t.is(await resource1.getString(), "MyInitialContent"); + t.is(await resource1!.getString(), "MyInitialContent"); t.is(await resource.getString(), "MyNewContent"); - t.is(await resource1.getString(), "MyInitialContent"); + t.is(await resource1!.getString(), "MyInitialContent"); - await dest.write(resource); + await dest!.write(resource); - const resource2 = await dest.byPath("/dest/writer/content/test.js"); + const resource2 = await dest!.byPath("/dest/writer/content/test.js"); t.is(await resource.getString(), "MyNewContent"); - t.is(await resource2.getString(), "MyNewContent"); + t.is(await resource2!.getString(), "MyNewContent"); }); test(adapter + ": Create resource, write and change path", async (t) => { const dest = await getAdapter({ fsBasePath: "./test/tmp/writer/", - virBasePath: "/dest/writer/" + virBasePath: "/dest/writer/", }); const resource = createResource({ path: "/dest/writer/path/test.js", - string: "MyInitialContent" + string: "MyInitialContent", }); - await dest.write(resource); + await dest!.write(resource); resource.setPath("/dest/writer/path/test2.js"); - const resourceOldPath = await dest.byPath("/dest/writer/path/test.js"); - const resourceNewPath = await dest.byPath("/dest/writer/path/test2.js"); + const resourceOldPath = await dest!.byPath("/dest/writer/path/test.js"); + const resourceNewPath = await dest!.byPath("/dest/writer/path/test2.js"); - t.is(await resource.getPath(), "/dest/writer/path/test2.js"); + t.is(resource.getPath(), "/dest/writer/path/test2.js"); t.truthy(resourceOldPath); - t.is(await resourceOldPath.getString(), await resource.getString()); - t.is(await resourceOldPath.getPath(), "/dest/writer/path/test.js"); - t.not(resourceNewPath); + t.is(await resourceOldPath!.getString(), await resource.getString()); + t.is(resourceOldPath!.getPath(), "/dest/writer/path/test.js"); + t.not(resourceNewPath, undefined); - await dest.write(resource); + await dest!.write(resource); - const resourceOldPath1 = await dest.byPath("/dest/writer/path/test.js"); - const resourceNewPath1 = await dest.byPath("/dest/writer/path/test2.js"); + const resourceOldPath1 = await dest!.byPath("/dest/writer/path/test.js"); + const resourceNewPath1 = await dest!.byPath("/dest/writer/path/test2.js"); - t.is(await resource.getPath(), "/dest/writer/path/test2.js"); + t.is(resource.getPath(), "/dest/writer/path/test2.js"); t.truthy(resourceNewPath1); - t.is(await resourceNewPath1.getString(), await resource.getString()); - t.is(await resourceNewPath1.getPath(), "/dest/writer/path/test2.js"); - t.not(resourceOldPath1); + t.is(await resourceNewPath1!.getString(), await resource.getString()); + t.is(resourceNewPath1!.getPath(), "/dest/writer/path/test2.js"); + t.not(resourceOldPath1, undefined); }); test(adapter + - ": Create a resource with a path different from the path configured in the adapter", async (t) => { - t.pass(2); + ": Create a resource with a path different from the path configured in the adapter", async (t) => { + t.pass("2"); const dest = await getAdapter({ fsBasePath: "./test/tmp/writer/", - virBasePath: "/dest2/writer/" + virBasePath: "/dest2/writer/", }); const resource = createResource({ path: "/dest2/tmp/test.js", - string: "MyContent" + string: "MyContent", }); - const error = await t.throwsAsync(dest.write(resource)); + const error = await t.throwsAsync(dest!.write(resource)); t.is(error.message, "Failed to write resource with virtual path '/dest2/tmp/test.js': Path must start with the " + "configured virtual base path of the adapter. Base path: '/dest2/writer/'", @@ -157,16 +167,16 @@ async function fileEqual(t, actual, expected) { }); test(adapter + - ": Create a resource with a path above the path configured in the adapter", async (t) => { - t.pass(2); + ": Create a resource with a path above the path configured in the adapter", async (t) => { + t.pass("2"); const dest = await getAdapter({ fsBasePath: "./test/tmp/writer/", - virBasePath: "/dest2/writer/" + virBasePath: "/dest2/writer/", }); const resource = createResource({ path: "/dest2/test.js", - string: "MyContent" + string: "MyContent", }); const error = await t.throwsAsync(dest.write(resource)); @@ -177,16 +187,16 @@ async function fileEqual(t, actual, expected) { }); test(adapter + - ": Create a resource with a path resolving outside the path configured in the adapter", async (t) => { - t.pass(2); + ": Create a resource with a path resolving outside the path configured in the adapter", async (t) => { + t.pass("2"); const dest = await getAdapter({ fsBasePath: "./test/tmp/writer/", - virBasePath: "/dest/writer/" + virBasePath: "/dest/writer/", }); const resource = createResource({ path: "/dest/writer/../relative.js", - string: "MyContent" + string: "MyContent", }); // Resource will already resolve relative path segments t.is(resource.getPath(), "/dest/relative.js", "Resource path resolved"); @@ -198,20 +208,20 @@ async function fileEqual(t, actual, expected) { await t.throwsAsync(dest.write(resource), { message: "Failed to write resource with virtual path '/dest/writer/../relative.js': " + - "Path must start with the configured virtual base path of the adapter. Base path: '/dest/writer/'" + "Path must start with the configured virtual base path of the adapter. Base path: '/dest/writer/'", }, "Threw with expected error message"); }); test(adapter + ": Filter resources", async (t) => { const source = createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" + virBasePath: "/app/", }); const filteredSource = createFilterReader({ reader: source, callback: (resource) => { return resource.getPath().endsWith(".js"); - } + }, }); const sourceResources = await source.byGlob("**"); t.is(sourceResources.length, 2, "Found two resources in source"); @@ -225,11 +235,11 @@ async function fileEqual(t, actual, expected) { test(adapter + ": Flatten resources", async (t) => { const source = await getAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); const transformedSource = createFlatReader({ - reader: source, - namespace: "app" + reader: source!, + namespace: "app", }); const resources = await transformedSource.byGlob("**/*.js"); @@ -240,14 +250,14 @@ async function fileEqual(t, actual, expected) { test(adapter + ": Link resources", async (t) => { const source = await getAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + virBasePath: "/resources/app/", }); const transformedSource = createLinkReader({ - reader: source, + reader: source!, pathMapping: { linkPath: "/wow/this/is/a/beautiful/path/just/wow/", - targetPath: "/resources/" - } + targetPath: "/resources/", + }, }); const resources = await transformedSource.byGlob("**/*.js"); diff --git a/test/lib/tracing/traceSummary.js b/test/lib/tracing/traceSummary.ts similarity index 78% rename from test/lib/tracing/traceSummary.js rename to test/lib/tracing/traceSummary.ts index 94bbd746..96428a40 100644 --- a/test/lib/tracing/traceSummary.js +++ b/test/lib/tracing/traceSummary.ts @@ -1,23 +1,32 @@ -import test from "ava"; -import sinon from "sinon"; -import esmock from "esmock"; +import anyTest, {type TestFn, type ExecutionContext} from "ava"; +import sinon, {type SinonStub} from "sinon"; +import esmock, {type MockFunction} from "esmock"; + +interface avaContext { + loggerStub: { + silly: SinonStub; + isLevelEnabled: (arg: boolean) => boolean; + }; + traceSummary: MockFunction; +} +const test = anyTest as TestFn; -async function createMock(t, isLevelEnabled=true) { +async function createMock(t: ExecutionContext, isLevelEnabled = true) { t.context.loggerStub = { silly: sinon.stub(), isLevelEnabled: () => { return isLevelEnabled; - } + }, }; - t.context.traceSummary = await esmock("../../../lib/tracing/traceSummary.js", { + t.context.traceSummary = await esmock("../../../src/tracing/traceSummary.js", { "@ui5/logger": { - getLogger: sinon.stub().returns(t.context.loggerStub) - } + getLogger: sinon.stub().returns(t.context.loggerStub), + }, }); return t.context; } -test.afterEach.always((t) => { +test.afterEach.always((_t) => { sinon.restore(); }); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 00000000..02c658af --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "outDir": "./lib", + "moduleResolution": "node16", + "module": "node16", + "target": "es2022", + "lib": ["ES2022"], + "strict": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..0aa09f59 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + // For creating a release build, only compile the src dir + "extends": "./tsconfig.base.json", + "include": ["src/**/*"], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..2f4ae369 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + // This is our configuration for development and linting, + // compared to "tsconfig.build.json" it also includes tests + // and other TypeScript sources in the project + // For this reason however, it should not be used to emit JavaScript (except for linting purposes), + // since the output directory would then contain "src" and "test" directories instead of just the content of "src" + "extends": "./tsconfig.base.json", + "include": [ + "src/**/*", + "test/**/*", + ], + "exclude": [ + "test/tmp/**/*", + "test/fixtures/**/*", + ] +} diff --git a/typedoc.config.js b/typedoc.config.js new file mode 100644 index 00000000..080f4c02 --- /dev/null +++ b/typedoc.config.js @@ -0,0 +1,10 @@ +/** @type {Partial} */ +const config = { + entryPoints: ["./src/"], + tsconfig: "tsconfig.build.json", + out: "jsdocs", + entryPointStrategy: "expand", + plugin: ["typedoc-plugin-rename-defaults"], +}; + +export default config;