diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a99de6..fffb4bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: - name: npm install and test run: | npm install + npm run build npm test env: CI: true diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 205c35d..959b03f 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -34,7 +34,9 @@ jobs: node-version: lts/* registry-url: "https://registry.npmjs.org" - - run: npm install + - run: | + npm install + npm run build if: ${{ steps.release.outputs.releases_created }} #----------------------------------------------------------------------------- @@ -87,9 +89,7 @@ jobs: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - name: Publish @eslint/object-schema package to JSR - run: | - npm run build --if-present - npx jsr publish + run: npx jsr publish working-directory: packages/object-schema if: ${{ steps.release.outputs['packages/object-schema--release_created'] }} @@ -120,9 +120,7 @@ jobs: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - name: Publish @eslint/config-array package to JSR - run: | - npm run build --if-present - npx jsr publish + run: npx jsr publish working-directory: packages/config-array if: ${{ steps.release.outputs['packages/config-array--release_created'] }} diff --git a/package.json b/package.json index f96d80c..644ba10 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "test": "npm test --workspaces --if-present", + "build": "node scripts/build.js", "lint": "eslint .", "lint:fix": "eslint --fix .", "fmt": "prettier --write ." diff --git a/packages/config-array/jsr.json b/packages/config-array/jsr.json index 8e20ed2..f97d38f 100644 --- a/packages/config-array/jsr.json +++ b/packages/config-array/jsr.json @@ -3,8 +3,13 @@ "version": "0.12.3", "exports": "./dist/esm/index.js", "publish": { - "exclude": [ - "!dist" + "include": [ + "dist/esm/index.js", + "dist/esm/index.d.ts", + "dist/esm/types.d.ts", + "README.md", + "jsr.json", + "LICENSE" ] } } diff --git a/packages/config-array/package.json b/packages/config-array/package.json index 5f8e787..9c0867c 100644 --- a/packages/config-array/package.json +++ b/packages/config-array/package.json @@ -14,6 +14,16 @@ "default": "./dist/esm/index.js" } }, + "files": [ + "dist/cjs/index.cjs", + "dist/cjs/index.d.cts", + "dist/cjs/types.d.ts", + "dist/esm/index.js", + "dist/esm/index.d.ts", + "dist/esm/types.d.ts", + "README.md", + "LICENSE" + ], "repository": { "type": "git", "url": "git+https://github.com/eslint/rewrite.git" @@ -23,8 +33,9 @@ }, "homepage": "https://github.com/eslint/rewrite#readme", "scripts": { - "build": "rollup -c", - "prepare": "npm run build", + "build:dedupe-types": "node ../../tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js", + "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json", + "pretest": "npm run build", "test": "mocha tests/" }, "keywords": [ @@ -39,10 +50,11 @@ "minimatch": "^3.0.5" }, "devDependencies": { + "@types/minimatch": "^3.0.5", "mocha": "^10.4.0", "rollup": "^4.16.2", - "typescript": "^5.4.5", - "rollup-plugin-copy": "^3.5.0" + "rollup-plugin-copy": "^3.5.0", + "typescript": "^5.4.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/packages/config-array/src/base-schema.js b/packages/config-array/src/base-schema.js index dd7ece0..782be15 100644 --- a/packages/config-array/src/base-schema.js +++ b/packages/config-array/src/base-schema.js @@ -3,16 +3,27 @@ * @author Nicholas C. Zakas */ +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +/** @typedef {import("@eslint/object-schema").PropertyDefinition} PropertyDefinition */ +/** @typedef {import("@eslint/object-schema").ObjectDefinition} ObjectDefinition */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ +/** + * A strategy that does nothing. + * @type {PropertyDefinition} + */ const NOOP_STRATEGY = { required: false, merge() { return undefined; }, - validate() { } + validate() {}, }; //------------------------------------------------------------------------------ @@ -21,7 +32,7 @@ const NOOP_STRATEGY = { /** * The base schema that every ConfigArray uses. - * @type Object + * @type {ObjectDefinition} */ export const baseSchema = Object.freeze({ name: { @@ -30,11 +41,11 @@ export const baseSchema = Object.freeze({ return undefined; }, validate(value) { - if (typeof value !== 'string') { - throw new TypeError('Property must be a string.'); + if (typeof value !== "string") { + throw new TypeError("Property must be a string."); } - } + }, }, files: NOOP_STRATEGY, - ignores: NOOP_STRATEGY + ignores: NOOP_STRATEGY, }); diff --git a/packages/config-array/src/config-array.js b/packages/config-array/src/config-array.js index 60f0293..3aadb0f 100644 --- a/packages/config-array/src/config-array.js +++ b/packages/config-array/src/config-array.js @@ -7,35 +7,77 @@ // Imports //------------------------------------------------------------------------------ -import path from 'path'; -import minimatch from 'minimatch'; -import createDebug from 'debug'; +import path from "node:path"; +import minimatch from "minimatch"; +import createDebug from "debug"; -import { ObjectSchema } from '@humanwhocodes/object-schema'; -import { baseSchema } from './base-schema.js'; -import { filesAndIgnoresSchema } from './files-and-ignores-schema.js'; +import { ObjectSchema } from "@eslint/object-schema"; +import { baseSchema } from "./base-schema.js"; +import { filesAndIgnoresSchema } from "./files-and-ignores-schema.js"; + +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +/** @typedef {import("@eslint/object-schema").PropertyDefinition} PropertyDefinition */ +/** @typedef {import("@eslint/object-schema").ObjectDefinition} ObjectDefinition */ +/** @typedef {import("./types.ts").BaseConfigObject} BaseConfigObject */ +/** @typedef {import("minimatch").IMinimatchStatic} IMinimatchStatic */ +/** @typedef {import("minimatch").IMinimatch} IMinimatch */ + +/* + * This is a bit of a hack to make TypeScript happy with the Rollup-created + * CommonJS file. Rollup doesn't do object destructuring for imported files + * and instead imports the default via `require()`. This messes up type checking + * for `ObjectSchema`. To work around that, we just import the type manually + * and give it a different name to use in the JSDoc comments. + */ +/** @typedef {import("@eslint/object-schema").ObjectSchema} ObjectSchemaInstance */ //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ const Minimatch = minimatch.Minimatch; +const debug = createDebug("@eslint/config-array"); + +/** + * A cache for minimatch instances. + * @type {Map} + */ const minimatchCache = new Map(); + +/** + * A cache for negated minimatch instances. + * @type {Map} + */ const negatedMinimatchCache = new Map(); -const debug = createDebug('@hwc/config-array'); +/** + * Options to use with minimatch. + * @type {Object} + */ const MINIMATCH_OPTIONS = { // matchBase: true, - dot: true + dot: true, }; -const CONFIG_TYPES = new Set(['array', 'function']); +/** + * The types of config objects that are supported. + * @type {Set} + */ +const CONFIG_TYPES = new Set(["array", "function"]); /** * Fields that are considered metadata and not part of the config object. + * @type {Set} */ -const META_FIELDS = new Set(['name']); +const META_FIELDS = new Set(["name"]); +/** + * A schema containing just files and ignores for early validation. + * @type {ObjectSchemaInstance} + */ const FILES_AND_IGNORES_SCHEMA = new ObjectSchema(filesAndIgnoresSchema); /** @@ -43,16 +85,15 @@ const FILES_AND_IGNORES_SCHEMA = new ObjectSchema(filesAndIgnoresSchema); * error message. */ class ConfigError extends Error { - /** * Creates a new instance. * @param {string} name The config object name causing the error. * @param {number} index The index of the config object in the array. - * @param {Error} source The source error. + * @param {Object} options The options for the error. + * @param {Error} [options.cause] The error that caused this error. + * @param {string} [options.message] The message to use for the error. */ constructor(name, index, { cause, message }) { - - const finalMessage = message || cause.message; super(`Config ${name}: ${finalMessage}`, { cause }); @@ -71,7 +112,7 @@ class ConfigError extends Error { * @type {string} * @readonly */ - this.name = 'ConfigError'; + this.name = "ConfigError"; /** * The index of the config object in the array. @@ -84,36 +125,36 @@ class ConfigError extends Error { /** * Gets the name of a config object. - * @param {object} config The config object to get the name of. + * @param {BaseConfigObject} config The config object to get the name of. * @returns {string} The name of the config object. - */ + */ function getConfigName(config) { - if (config && typeof config.name === 'string' && config.name) { + if (config && typeof config.name === "string" && config.name) { return `"${config.name}"`; } - return '(unnamed)'; + return "(unnamed)"; } /** * Rethrows a config error with additional information about the config object. - * @param {object} config The config object to get the name of. + * @param {object} config The config object to get the name of. * @param {number} index The index of the config object in the array. * @param {Error} error The error to rethrow. * @throws {ConfigError} When the error is rethrown for a config. */ function rethrowConfigError(config, index, error) { const configName = getConfigName(config); - throw new ConfigError(configName, index, error); + throw new ConfigError(configName, index, { cause: error }); } /** * Shorthand for checking if a value is a string. * @param {any} value The value to check. - * @returns {boolean} True if a string, false if not. + * @returns {boolean} True if a string, false if not. */ function isString(value) { - return typeof value === 'string'; + return typeof value === "string"; } /** @@ -126,33 +167,38 @@ function isString(value) { * @throws {ConfigError} If the files and ignores keys of a config object are not valid. */ function assertValidBaseConfig(config, index) { - if (config === null) { - throw new ConfigError(getConfigName(config), index, { message: 'Unexpected null config.' }); + throw new ConfigError(getConfigName(config), index, { + message: "Unexpected null config.", + }); } if (config === undefined) { - throw new ConfigError(getConfigName(config), index, { message: 'Unexpected undefined config.' }); + throw new ConfigError(getConfigName(config), index, { + message: "Unexpected undefined config.", + }); } - if (typeof config !== 'object') { - throw new ConfigError(getConfigName(config), index, { message: 'Unexpected non-object config.' }); + if (typeof config !== "object") { + throw new ConfigError(getConfigName(config), index, { + message: "Unexpected non-object config.", + }); } - const validateConfig = { }; - - if ('files' in config) { + const validateConfig = {}; + + if ("files" in config) { validateConfig.files = config.files; } - - if ('ignores' in config) { + + if ("ignores" in config) { validateConfig.ignores = config.ignores; } try { FILES_AND_IGNORES_SCHEMA.validate(validateConfig); } catch (validationError) { - rethrowConfigError(config, index, { cause: validationError }); + rethrowConfigError(config, index, validationError); } } @@ -162,10 +208,9 @@ function assertValidBaseConfig(config, index) { * @param {string} filepath The file path to match. * @param {string} pattern The glob pattern to match against. * @param {object} options The minimatch options to use. - * @returns + * @returns */ function doMatch(filepath, pattern, options = {}) { - let cache = minimatchCache; if (options.flipNegate) { @@ -175,7 +220,10 @@ function doMatch(filepath, pattern, options = {}) { let matcher = cache.get(pattern); if (!matcher) { - matcher = new Minimatch(pattern, Object.assign({}, MINIMATCH_OPTIONS, options)); + matcher = new Minimatch( + pattern, + Object.assign({}, MINIMATCH_OPTIONS, options), + ); cache.set(pattern, matcher); } @@ -193,15 +241,14 @@ function doMatch(filepath, pattern, options = {}) { * @throws {TypeError} When a config function returns a function. */ async function normalize(items, context, extraConfigTypes) { - - const allowFunctions = extraConfigTypes.includes('function'); - const allowArrays = extraConfigTypes.includes('array'); + const allowFunctions = extraConfigTypes.includes("function"); + const allowArrays = extraConfigTypes.includes("array"); async function* flatTraverse(array) { for (let item of array) { - if (typeof item === 'function') { + if (typeof item === "function") { if (!allowFunctions) { - throw new TypeError('Unexpected function.'); + throw new TypeError("Unexpected function."); } item = item(context); @@ -212,11 +259,13 @@ async function normalize(items, context, extraConfigTypes) { if (Array.isArray(item)) { if (!allowArrays) { - throw new TypeError('Unexpected array.'); + throw new TypeError("Unexpected array."); } yield* flatTraverse(item); - } else if (typeof item === 'function') { - throw new TypeError('A config function can only return an object or array.'); + } else if (typeof item === "function") { + throw new TypeError( + "A config function can only return an object or array.", + ); } else { yield item; } @@ -248,33 +297,34 @@ async function normalize(items, context, extraConfigTypes) { * @throws {TypeError} When a config function returns a function. */ function normalizeSync(items, context, extraConfigTypes) { - - const allowFunctions = extraConfigTypes.includes('function'); - const allowArrays = extraConfigTypes.includes('array'); + const allowFunctions = extraConfigTypes.includes("function"); + const allowArrays = extraConfigTypes.includes("array"); function* flatTraverse(array) { for (let item of array) { - if (typeof item === 'function') { - + if (typeof item === "function") { if (!allowFunctions) { - throw new TypeError('Unexpected function.'); + throw new TypeError("Unexpected function."); } item = item(context); if (item.then) { - throw new TypeError('Async config functions are not supported.'); + throw new TypeError( + "Async config functions are not supported.", + ); } } if (Array.isArray(item)) { - if (!allowArrays) { - throw new TypeError('Unexpected array.'); + throw new TypeError("Unexpected array."); } yield* flatTraverse(item); - } else if (typeof item === 'function') { - throw new TypeError('A config function can only return an object or array.'); + } else if (typeof item === "function") { + throw new TypeError( + "A config function can only return an object or array.", + ); } else { yield item; } @@ -287,47 +337,41 @@ function normalizeSync(items, context, extraConfigTypes) { /** * Determines if a given file path should be ignored based on the given * matcher. - * @param {Array boolean>} ignores The ignore patterns to check. + * @param {Array boolean)>} ignores The ignore patterns to check. * @param {string} filePath The absolute path of the file to check. * @param {string} relativeFilePath The relative path of the file to check. * @returns {boolean} True if the path should be ignored and false if not. */ function shouldIgnorePath(ignores, filePath, relativeFilePath) { - // all files outside of the basePath are ignored - if (relativeFilePath.startsWith('..')) { + if (relativeFilePath.startsWith("..")) { return true; } return ignores.reduce((ignored, matcher) => { - if (!ignored) { - - if (typeof matcher === 'function') { + if (typeof matcher === "function") { return matcher(filePath); } // don't check negated patterns because we're not ignored yet - if (!matcher.startsWith('!')) { + if (!matcher.startsWith("!")) { return doMatch(relativeFilePath, matcher); } // otherwise we're still not ignored return false; - } // only need to check negated patterns because we're ignored - if (typeof matcher === 'string' && matcher.startsWith('!')) { + if (typeof matcher === "string" && matcher.startsWith("!")) { return !doMatch(relativeFilePath, matcher, { - flipNegate: true + flipNegate: true, }); } return ignored; - }, false); - } /** @@ -340,7 +384,6 @@ function shouldIgnorePath(ignores, filePath, relativeFilePath) { * false if not. */ function pathMatchesIgnores(filePath, basePath, config) { - /* * For both files and ignores, functions are passed the absolute * file path while strings are compared against the relative @@ -348,11 +391,12 @@ function pathMatchesIgnores(filePath, basePath, config) { */ const relativeFilePath = path.relative(basePath, filePath); - return Object.keys(config).filter(key => !META_FIELDS.has(key)).length > 1 && - !shouldIgnorePath(config.ignores, filePath, relativeFilePath); + return ( + Object.keys(config).filter(key => !META_FIELDS.has(key)).length > 1 && + !shouldIgnorePath(config.ignores, filePath, relativeFilePath) + ); } - /** * Determines if a given file path is matched by a config. If the config * has no `files` field, then it matches; otherwise, if a `files` field @@ -365,7 +409,6 @@ function pathMatchesIgnores(filePath, basePath, config) { * false if not. */ function pathMatches(filePath, basePath, config) { - /* * For both files and ignores, functions are passed the absolute * file path while strings are compared against the relative @@ -375,12 +418,11 @@ function pathMatches(filePath, basePath, config) { // match both strings and functions const match = pattern => { - if (isString(pattern)) { return doMatch(relativeFilePath, pattern); } - if (typeof pattern === 'function') { + if (typeof pattern === "function") { return pattern(filePath); } @@ -401,7 +443,11 @@ function pathMatches(filePath, basePath, config) { * if there are any files to ignore. */ if (filePathMatchesPattern && config.ignores) { - filePathMatchesPattern = !shouldIgnorePath(config.ignores, filePath, relativeFilePath); + filePathMatchesPattern = !shouldIgnorePath( + config.ignores, + filePath, + relativeFilePath, + ); } return filePathMatchesPattern; @@ -409,14 +455,16 @@ function pathMatches(filePath, basePath, config) { /** * Ensures that a ConfigArray has been normalized. - * @param {ConfigArray} configArray The ConfigArray to check. + * @param {ConfigArray} configArray The ConfigArray to check. * @returns {void} * @throws {Error} When the `ConfigArray` is not normalized. */ function assertNormalized(configArray) { // TODO: Throw more verbose error if (!configArray.isNormalized()) { - throw new Error('ConfigArray must be normalized to perform this operation.'); + throw new Error( + "ConfigArray must be normalized to perform this operation.", + ); } } @@ -428,12 +476,16 @@ function assertNormalized(configArray) { */ function assertExtraConfigTypes(extraConfigTypes) { if (extraConfigTypes.length > 2) { - throw new TypeError('configTypes must be an array with at most two items.'); + throw new TypeError( + "configTypes must be an array with at most two items.", + ); } for (const configType of extraConfigTypes) { if (!CONFIG_TYPES.has(configType)) { - throw new TypeError(`Unexpected config type "${configType}" found. Expected one of: "object", "array", "function".`); + throw new TypeError( + `Unexpected config type "${configType}" found. Expected one of: "object", "array", "function".`, + ); } } } @@ -443,11 +495,11 @@ function assertExtraConfigTypes(extraConfigTypes) { //------------------------------------------------------------------------------ export const ConfigArraySymbol = { - isNormalized: Symbol('isNormalized'), - configCache: Symbol('configCache'), - schema: Symbol('schema'), - finalizeConfig: Symbol('finalizeConfig'), - preprocessConfig: Symbol('preprocessConfig') + isNormalized: Symbol("isNormalized"), + configCache: Symbol("configCache"), + schema: Symbol("schema"), + finalizeConfig: Symbol("finalizeConfig"), + preprocessConfig: Symbol("preprocessConfig"), }; // used to store calculate data for faster lookup @@ -458,24 +510,26 @@ const dataCache = new WeakMap(); * those config objects. */ export class ConfigArray extends Array { - /** * Creates a new instance of ConfigArray. * @param {Iterable|Function|Object} configs An iterable yielding config * objects, or a config function, or a config object. + * @param {Object} options The options for the ConfigArray. * @param {string} [options.basePath=""] The path of the config file * @param {boolean} [options.normalized=false] Flag indicating if the * configs have already been normalized. - * @param {Object} [options.schema] The additional schema + * @param {Object} [options.schema] The additional schema * definitions to use for the ConfigArray schema. - * @param {Array} [options.configTypes] List of config types supported. + * @param {Array} [options.extraConfigTypes] List of config types supported. */ - constructor(configs, { - basePath = '', - normalized = false, - schema: customSchema, - extraConfigTypes = [] - } = {} + constructor( + configs, + { + basePath = "", + normalized = false, + schema: customSchema, + extraConfigTypes = [], + } = {}, ) { super(); @@ -490,11 +544,11 @@ export class ConfigArray extends Array { /** * The schema used for validating and merging configs. * @property schema - * @type ObjectSchema + * @type {ObjectSchemaInstance} * @private */ this[ConfigArraySymbol.schema] = new ObjectSchema( - Object.assign({}, customSchema, baseSchema) + Object.assign({}, customSchema, baseSchema), ); /** @@ -509,10 +563,10 @@ export class ConfigArray extends Array { /** * The supported config types. - * @property configTypes * @type {Array} */ - this.extraConfigTypes = Object.freeze([...extraConfigTypes]); + this.extraConfigTypes = [...extraConfigTypes]; + Object.freeze(this.extraConfigTypes); /** * A cache to store calculated configs for faster repeat lookup. @@ -527,7 +581,7 @@ export class ConfigArray extends Array { explicitMatches: new Map(), directoryMatches: new Map(), files: undefined, - ignores: undefined + ignores: undefined, }); // load the configs into this array @@ -536,15 +590,14 @@ export class ConfigArray extends Array { } else { this.push(configs); } - } /** * Prevent normal array methods from creating a new `ConfigArray` instance. - * This is to ensure that methods such as `slice()` won't try to create a + * This is to ensure that methods such as `slice()` won't try to create a * new instance of `ConfigArray` behind the scenes as doing so may throw * an error due to the different constructor signature. - * @returns {Function} The `Array` constructor. + * @type {ArrayConstructor} The `Array` constructor. */ static get [Symbol.species]() { return Array; @@ -558,7 +611,6 @@ export class ConfigArray extends Array { * @returns {Array} An array of matchers. */ get files() { - assertNormalized(this); // if this data has been cached, retrieve it @@ -595,7 +647,6 @@ export class ConfigArray extends Array { * @returns {string[]} An array of string patterns and functions to be ignored. */ get ignores() { - assertNormalized(this); // if this data has been cached, retrieve it @@ -610,13 +661,16 @@ export class ConfigArray extends Array { const result = []; for (const config of this) { - /* * We only count ignores if there are no other keys in the object. * In this case, it acts list a globally ignored pattern. If there * are additional keys, then ignores act like exclusions. */ - if (config.ignores && Object.keys(config).filter(key => !META_FIELDS.has(key)).length === 1) { + if ( + config.ignores && + Object.keys(config).filter(key => !META_FIELDS.has(key)) + .length === 1 + ) { result.push(...config.ignores); } } @@ -639,15 +693,22 @@ export class ConfigArray extends Array { /** * Normalizes a config array by flattening embedded arrays and executing * config functions. - * @param {ConfigContext} context The context object for config functions. + * @param {Object} [context] The context object for config functions. * @returns {Promise} The current ConfigArray instance. */ async normalize(context = {}) { - if (!this.isNormalized()) { - const normalizedConfigs = await normalize(this, context, this.extraConfigTypes); + const normalizedConfigs = await normalize( + this, + context, + this.extraConfigTypes, + ); this.length = 0; - this.push(...normalizedConfigs.map(this[ConfigArraySymbol.preprocessConfig].bind(this))); + this.push( + ...normalizedConfigs.map( + this[ConfigArraySymbol.preprocessConfig].bind(this), + ), + ); this.forEach(assertValidBaseConfig); this[ConfigArraySymbol.isNormalized] = true; @@ -661,15 +722,22 @@ export class ConfigArray extends Array { /** * Normalizes a config array by flattening embedded arrays and executing * config functions. - * @param {ConfigContext} context The context object for config functions. + * @param {Object} [context] The context object for config functions. * @returns {ConfigArray} The current ConfigArray instance. */ normalizeSync(context = {}) { - if (!this.isNormalized()) { - const normalizedConfigs = normalizeSync(this, context, this.extraConfigTypes); + const normalizedConfigs = normalizeSync( + this, + context, + this.extraConfigTypes, + ); this.length = 0; - this.push(...normalizedConfigs.map(this[ConfigArraySymbol.preprocessConfig].bind(this))); + this.push( + ...normalizedConfigs.map( + this[ConfigArraySymbol.preprocessConfig].bind(this), + ), + ); this.forEach(assertValidBaseConfig); this[ConfigArraySymbol.isNormalized] = true; @@ -712,7 +780,6 @@ export class ConfigArray extends Array { * or false if not. */ isExplicitMatch(filePath) { - assertNormalized(this); const cache = dataCache.get(this); @@ -720,7 +787,7 @@ export class ConfigArray extends Array { // first check the cache to avoid duplicate work let result = cache.explicitMatches.get(filePath); - if (typeof result == 'boolean') { + if (typeof result == "boolean") { return result; } @@ -738,7 +805,6 @@ export class ConfigArray extends Array { // filePath isn't automatically ignored, so try to find a match for (const config of this) { - if (!config.files) { continue; } @@ -759,7 +825,6 @@ export class ConfigArray extends Array { * @returns {Object} The config object for this file. */ getConfig(filePath) { - assertNormalized(this); const cache = this[ConfigArraySymbol.configCache]; @@ -800,9 +865,7 @@ export class ConfigArray extends Array { const universalPattern = /\/\*{1,2}$/; this.forEach((config, index) => { - if (!config.files) { - if (!config.ignores) { debug(`Anonymous universal config found for ${filePath}`); matchingConfigIndices.push(index); @@ -810,12 +873,16 @@ export class ConfigArray extends Array { } if (pathMatchesIgnores(filePath, this.basePath, config)) { - debug(`Matching config found for ${filePath} (based on ignores: ${config.ignores})`); + debug( + `Matching config found for ${filePath} (based on ignores: ${config.ignores})`, + ); matchingConfigIndices.push(index); return; } - - debug(`Skipped config found for ${filePath} (based on ignores: ${config.ignores})`); + + debug( + `Skipped config found for ${filePath} (based on ignores: ${config.ignores})`, + ); return; } @@ -826,26 +893,25 @@ export class ConfigArray extends Array { * a file with a specific extensions such as *.js. */ - const universalFiles = config.files.filter( - pattern => universalPattern.test(pattern) + const universalFiles = config.files.filter(pattern => + universalPattern.test(pattern), ); // universal patterns were found so we need to check the config twice if (universalFiles.length) { - - debug('Universal files patterns found. Checking carefully.'); + debug("Universal files patterns found. Checking carefully."); const nonUniversalFiles = config.files.filter( - pattern => !universalPattern.test(pattern) + pattern => !universalPattern.test(pattern), ); // check that the config matches without the non-universal files first if ( - nonUniversalFiles.length && - pathMatches( - filePath, this.basePath, - { files: nonUniversalFiles, ignores: config.ignores } - ) + nonUniversalFiles.length && + pathMatches(filePath, this.basePath, { + files: nonUniversalFiles, + ignores: config.ignores, + }) ) { debug(`Matching config found for ${filePath}`); matchingConfigIndices.push(index); @@ -856,10 +922,10 @@ export class ConfigArray extends Array { // if there wasn't a match then check if it matches with universal files if ( universalFiles.length && - pathMatches( - filePath, this.basePath, - { files: universalFiles, ignores: config.ignores } - ) + pathMatches(filePath, this.basePath, { + files: universalFiles, + ignores: config.ignores, + }) ) { debug(`Matching config found for ${filePath}`); matchingConfigIndices.push(index); @@ -877,7 +943,6 @@ export class ConfigArray extends Array { matchFound = true; return; } - }); // if matching both files and ignores, there will be no config to create @@ -893,7 +958,6 @@ export class ConfigArray extends Array { finalConfig = cache.get(matchingConfigIndices.toString()); if (finalConfig) { - // also store for filename for faster lookup next time cache.set(filePath, finalConfig); @@ -904,11 +968,14 @@ export class ConfigArray extends Array { finalConfig = matchingConfigIndices.reduce((result, index) => { try { - return this[ConfigArraySymbol.schema].merge(result, this[index]); + return this[ConfigArraySymbol.schema].merge( + result, + this[index], + ); } catch (validationError) { - rethrowConfigError(this[index], index, { cause: validationError}); + rethrowConfigError(this[index], index, validationError); } - }, {}, this); + }, {}); finalConfig = this[ConfigArraySymbol.finalizeConfig](finalConfig); @@ -939,7 +1006,7 @@ export class ConfigArray extends Array { /** * Determines if the given directory is ignored based on the configs. - * This checks only default `ignores` that don't have `files` in the + * This checks only default `ignores` that don't have `files` in the * same config. A pattern such as `/foo` be considered to ignore the directory * while a pattern such as `/foo/**` is not considered to ignore the * directory because it is matching files. @@ -949,13 +1016,13 @@ export class ConfigArray extends Array { * @throws {Error} When the `ConfigArray` is not normalized. */ isDirectoryIgnored(directoryPath) { - assertNormalized(this); - const relativeDirectoryPath = path.relative(this.basePath, directoryPath) - .replace(/\\/g, '/'); + const relativeDirectoryPath = path + .relative(this.basePath, directoryPath) + .replace(/\\/g, "/"); - if (relativeDirectoryPath.startsWith('..')) { + if (relativeDirectoryPath.startsWith("..")) { return true; } @@ -966,8 +1033,8 @@ export class ConfigArray extends Array { return cache.get(relativeDirectoryPath); } - const directoryParts = relativeDirectoryPath.split('/'); - let relativeDirectoryToCheck = ''; + const directoryParts = relativeDirectoryPath.split("/"); + let relativeDirectoryToCheck = ""; let result = false; /* @@ -975,22 +1042,20 @@ export class ConfigArray extends Array { * ignored parent directory cannot have any descendants unignored, * we need to check every directory starting at the parent all * the way down to the actual requested directory. - * + * * We aggressively cache all of this info to make sure we don't * have to recalculate everything for every call. */ do { - - relativeDirectoryToCheck += directoryParts.shift() + '/'; + relativeDirectoryToCheck += directoryParts.shift() + "/"; result = shouldIgnorePath( this.ignores, path.join(this.basePath, relativeDirectoryToCheck), - relativeDirectoryToCheck + relativeDirectoryToCheck, ); cache.set(relativeDirectoryToCheck, result); - } while (!result && directoryParts.length); // also cache the result for the requested path @@ -998,5 +1063,4 @@ export class ConfigArray extends Array { return result; } - } diff --git a/packages/config-array/src/files-and-ignores-schema.js b/packages/config-array/src/files-and-ignores-schema.js index 8762934..88b285d 100644 --- a/packages/config-array/src/files-and-ignores-schema.js +++ b/packages/config-array/src/files-and-ignores-schema.js @@ -3,6 +3,13 @@ * @author Nicholas C. Zakas */ +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +/** @typedef {import("@eslint/object-schema").PropertyDefinition} PropertyDefinition */ +/** @typedef {import("@eslint/object-schema").ObjectDefinition} ObjectDefinition */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -15,7 +22,7 @@ */ function assertIsArray(value) { if (!Array.isArray(value)) { - throw new TypeError('Expected value to be an array.'); + throw new TypeError("Expected value to be an array."); } } @@ -25,11 +32,17 @@ function assertIsArray(value) { * @returns {void} * @throws {TypeError} When the value is not an array of strings and functions. */ -function assertIsArrayOfStringsAndFunctions(value, name) { - assertIsArray(value, name); +function assertIsArrayOfStringsAndFunctions(value) { + assertIsArray(value); - if (value.some(item => typeof item !== 'string' && typeof item !== 'function')) { - throw new TypeError('Expected array to only contain strings and functions.'); + if ( + value.some( + item => typeof item !== "string" && typeof item !== "function", + ) + ) { + throw new TypeError( + "Expected array to only contain strings and functions.", + ); } } @@ -41,7 +54,7 @@ function assertIsArrayOfStringsAndFunctions(value, name) { */ function assertIsNonEmptyArray(value) { if (!Array.isArray(value) || value.length === 0) { - throw new TypeError('Expected value to be a non-empty array.'); + throw new TypeError("Expected value to be a non-empty array."); } } @@ -51,7 +64,7 @@ function assertIsNonEmptyArray(value) { /** * The schema for `files` and `ignores` that every ConfigArray uses. - * @type Object + * @type {ObjectDefinition} */ export const filesAndIgnoresSchema = Object.freeze({ files: { @@ -60,7 +73,6 @@ export const filesAndIgnoresSchema = Object.freeze({ return undefined; }, validate(value) { - // first check if it's an array assertIsNonEmptyArray(value); @@ -68,18 +80,22 @@ export const filesAndIgnoresSchema = Object.freeze({ value.forEach(item => { if (Array.isArray(item)) { assertIsArrayOfStringsAndFunctions(item); - } else if (typeof item !== 'string' && typeof item !== 'function') { - throw new TypeError('Items must be a string, a function, or an array of strings and functions.'); + } else if ( + typeof item !== "string" && + typeof item !== "function" + ) { + throw new TypeError( + "Items must be a string, a function, or an array of strings and functions.", + ); } }); - - } + }, }, ignores: { required: false, merge() { return undefined; }, - validate: assertIsArrayOfStringsAndFunctions - } + validate: assertIsArrayOfStringsAndFunctions, + }, }); diff --git a/packages/config-array/src/types.ts b/packages/config-array/src/types.ts new file mode 100644 index 0000000..dd9bbd3 --- /dev/null +++ b/packages/config-array/src/types.ts @@ -0,0 +1,23 @@ +/** + * @fileoverview Types for the config-array package. + * @author Nicholas C. Zakas + */ + +export interface BaseConfigObject { + + /** + * The files to include. + */ + files?: string[]; + + /** + * The files to exclude. + */ + ignores?: string[]; + + /** + * The name of the config object. + */ + name?: string; + +} diff --git a/packages/config-array/tsconfig.cjs.json b/packages/config-array/tsconfig.cjs.json index 7581927..9e63c30 100644 --- a/packages/config-array/tsconfig.cjs.json +++ b/packages/config-array/tsconfig.cjs.json @@ -3,7 +3,7 @@ "files": ["dist/cjs/index.cjs"], "compilerOptions": { "outDir": "dist/cjs", - "moduleResolution": "Node", - "module": "CommonJS" + "moduleResolution": "Bundler", + "module": "Preserve" }, } diff --git a/packages/object-schema/tsconfig.cjs.json b/packages/object-schema/tsconfig.cjs.json index 7581927..9e63c30 100644 --- a/packages/object-schema/tsconfig.cjs.json +++ b/packages/object-schema/tsconfig.cjs.json @@ -3,7 +3,7 @@ "files": ["dist/cjs/index.cjs"], "compilerOptions": { "outDir": "dist/cjs", - "moduleResolution": "Node", - "module": "CommonJS" + "moduleResolution": "Bundler", + "module": "Preserve" }, } diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..d21ae98 --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,126 @@ +/** + * @fileoverview Build script for the project. Because we are using a monorepo, + * we need to build each package in the correct order. Otherwise, the type + * definitions for the packages that depend on other packages won't be correct. + * @author Nicholas C. Zakas + */ + +/* eslint no-console: off */ +/* global console */ + +//------------------------------------------------------------------------------ +// Imports +//------------------------------------------------------------------------------ + +import { execSync } from "node:child_process"; +import path from "node:path"; +import fsp from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +//----------------------------------------------------------------------------- +// Data +//----------------------------------------------------------------------------- + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const PACKAGES_DIR = path.resolve(__dirname, "..", "packages"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Gets a list of directories in the packages directory. + * @returns {Promise} A promise that resolves with an array of package directories. + */ +async function getPackageDirs() { + const packageDirs = await fsp.readdir(PACKAGES_DIR); + return packageDirs.map(entry => `packages/${entry}`); +} + +/** + * Calculates the dependencies between packages. + * @param {Array} packageDirs An array of package directories. + * @returns {Map>} A map of package names to the set of dependencies. + */ +async function calculatePackageDependencies(packageDirs) { + return new Map( + await Promise.all( + packageDirs.map(async packageDir => { + const packageJson = await fsp.readFile( + path.join(packageDir, "package.json"), + "utf8", + ); + const pkg = JSON.parse(packageJson); + const dependencies = new Set(); + + if (pkg.dependencies) { + for (const dep of Object.keys(pkg.dependencies)) { + if (dep.startsWith("@eslint")) { + dependencies.add(dep); + } + } + } + + return [ + pkg.name, + { name: pkg.name, dir: packageDir, dependencies }, + ]; + }), + ), + ); +} + +/** + * Creates an array of directories to be built in order to sastify dependencies. + * @param {Map}} dependencies The + * dependencies between packages. + * @returns {Array} An array of directories to be built in order. + */ +function createBuildOrder(dependencies) { + const buildOrder = []; + const seen = new Set(); + + function visit(name) { + if (!seen.has(name)) { + seen.add(name); + const { dependencies: deps, dir } = dependencies.get(name); + deps.forEach(visit); + buildOrder.push(dir); + } + } + + dependencies.forEach((value, key) => { + visit(key); + }); + + return buildOrder; +} + +/** + * Builds the packages in the correct order. + * @param {Array} packageDirs An array of directories to build in order. + * @returns {void} + */ +function buildPackages(packageDirs) { + console.log(`Building packages in this order: ${packageDirs.join(", ")}`); + + for (const packageDir of packageDirs) { + console.log(`Building ${packageDir}...`); + execSync(`npm run build -w ${packageDir} --if-present`, { + stdio: "inherit", + }); + } + + console.log("Done building packages."); +} + +//------------------------------------------------------------------------------ +// Main Script +//------------------------------------------------------------------------------ + +const packageDirs = await getPackageDirs(); +const dependencies = await calculatePackageDependencies(packageDirs); +const buildOrder = createBuildOrder(dependencies); + +buildPackages(buildOrder); diff --git a/tools/dedupe-types.js b/tools/dedupe-types.js new file mode 100644 index 0000000..1f0b78f --- /dev/null +++ b/tools/dedupe-types.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Strips typedef aliases from the rolled-up file. This + * is necessary because the TypeScript compiler throws an error when + * it encounters a duplicate typedef. + * + * Usage: + * node scripts/strip-typedefs.js filename1.js filename2.js ... + * + * @author Nicholas C. Zakas + */ +/* global process */ +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import fs from "node:fs"; + +//----------------------------------------------------------------------------- +// Main +//----------------------------------------------------------------------------- + +// read files from the command line +const files = process.argv.slice(2); + +files.forEach(filePath => { + const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/g); + const typedefs = new Set(); + + const remainingLines = lines.filter(line => { + if (!line.startsWith("/** @typedef {import")) { + return true; + } + + if (typedefs.has(line)) { + return false; + } + + typedefs.add(line); + return true; + }); + + fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8"); +});