diff --git a/lib/index.js b/lib/index.js index 594f88e..d201c8c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,9 +5,9 @@ "use strict"; const { EventEmitter } = require("events"); -const globToRegExp = require("glob-to-regexp"); const LinkResolver = require("./LinkResolver"); const getWatcherManager = require("./getWatcherManager"); +const globToRegExp = require("./util/globToRegExp"); const watchEventSource = require("./watchEventSource"); /** @typedef {import("./getWatcherManager").WatcherManager} WatcherManager */ @@ -45,6 +45,7 @@ const watchEventSource = require("./watchEventSource"); /** @typedef {`scan (${string})` | "change" | "rename" | `watch ${string}` | `directory-removed ${string}`} EventType */ /** @typedef {{ safeTime: number, timestamp: number, accuracy: number }} Entry */ /** @typedef {{ safeTime: number }} OnlySafeTimeEntry */ + // eslint-disable-next-line jsdoc/ts-no-empty-object-type /** @typedef {{ }} ExistenceOnlyTimeEntry */ /** @typedef {Map} TimeInfoEntries */ @@ -73,8 +74,7 @@ const stringToRegexp = (ignored) => { if (ignored.length === 0) { return; } - const { source } = globToRegExp(ignored, { globstar: true, extended: true }); - return `${source.slice(0, -1)}(?:$|\\/)`; + return `^${globToRegExp(ignored)}(?:$|\\/)`; }; /** @@ -568,4 +568,27 @@ class Watchpack extends EventEmitter { } } -module.exports = Watchpack; +/** + * @template A + * @template B + * @param {A} obj input a + * @param {B} exports input b + * @returns {A & B} merged + */ +const mergeExports = (obj, exports) => { + const descriptors = Object.getOwnPropertyDescriptors(exports); + Object.defineProperties(obj, descriptors); + return /** @type {A & B} */ (Object.freeze(obj)); +}; + +/** @typedef {typeof Watchpack & { util: { readonly globToRegExp: typeof globToRegExp } }} WatchpackExports */ + +module.exports = /** @type {WatchpackExports} */ ( + mergeExports(Watchpack, { + util: { + get globToRegExp() { + return globToRegExp; + }, + }, + }) +); diff --git a/lib/util/globToRegExp.js b/lib/util/globToRegExp.js new file mode 100644 index 0000000..1fd1ded --- /dev/null +++ b/lib/util/globToRegExp.js @@ -0,0 +1,142 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Haijie Xie @hai-x +*/ + +"use strict"; + +// Based on https://github.com/fitzgen/glob-to-regexp (MIT) +// Specialized for watchpack: `extended` and `globstar` always enabled. +// Returns the regexp source without `^`/`$` anchors. + +const CC_EXCLAMATION = 33; // "!" +const CC_DOLLAR = 36; // "$" +const CC_LEFT_PARENTHESIS = 40; // "(" +const CC_RIGHT_PARENTHESIS = 41; // ")" +const CC_ASTERISK = 42; // "*" +const CC_PLUS = 43; // "+" +const CC_COMMA = 44; // "," +const CC_DOT = 46; // "." +const CC_SLASH = 47; // "/" +const CC_EQUAL = 61; // "=" +const CC_QUESTION_MARK = 63; // "?" +const CC_LEFT_BRACKET = 91; // "[" +const CC_RIGHT_BRACKET = 93; // "]" +const CC_CARET = 94; // "^" +const CC_LEFT_BRACE = 123; // "{" +const CC_PIPE = 124; // "|" +const CC_RIGHT_BRACE = 125; // "}" + +/** + * @param {string} glob glob pattern + * @returns {string} regexp source without anchors + */ +module.exports = (glob) => { + if (typeof glob !== "string") { + throw new TypeError("Expected a string"); + } + + const len = glob.length; + let reStr = ""; + let inGroup = false; + // Start of the current run of literal characters, copied with one slice + let literalStart = 0; + + for (let i = 0; i < len; i++) { + const cc = glob.charCodeAt(i); + const tokenStart = i; + let mapped; + + switch (cc) { + case CC_SLASH: + mapped = "\\/"; + break; + case CC_DOLLAR: + mapped = "\\$"; + break; + case CC_CARET: + mapped = "\\^"; + break; + case CC_PLUS: + mapped = "\\+"; + break; + case CC_DOT: + mapped = "\\."; + break; + case CC_LEFT_PARENTHESIS: + mapped = "\\("; + break; + case CC_RIGHT_PARENTHESIS: + mapped = "\\)"; + break; + case CC_EQUAL: + mapped = "\\="; + break; + case CC_EXCLAMATION: + mapped = "\\!"; + break; + case CC_PIPE: + mapped = "\\|"; + break; + case CC_QUESTION_MARK: + mapped = "."; + break; + case CC_LEFT_BRACKET: + mapped = "["; + break; + case CC_RIGHT_BRACKET: + mapped = "]"; + break; + case CC_LEFT_BRACE: + inGroup = true; + mapped = "("; + break; + case CC_RIGHT_BRACE: + inGroup = false; + mapped = ")"; + break; + case CC_COMMA: + mapped = inGroup ? "|" : "\\,"; + break; + case CC_ASTERISK: { + const atStart = i === 0; + const afterSlash = !atStart && glob.charCodeAt(i - 1) === CC_SLASH; + let starCount = 1; + while (i + 1 < len && glob.charCodeAt(i + 1) === CC_ASTERISK) { + starCount++; + i++; + } + const atEnd = i + 1 === len; + const beforeSlash = !atEnd && glob.charCodeAt(i + 1) === CC_SLASH; + if ( + starCount > 1 && + (atStart || afterSlash) && + (atEnd || beforeSlash) + ) { + // Globstar segment, matches zero or more path segments + mapped = "((?:[^/]*(?:\\/|$))*)"; + i++; // Move over the "/" + } else { + // Not a globstar, matches one path segment + mapped = "([^/]*)"; + } + break; + } + default: + // Literal character, extend the current run + continue; + } + + if (literalStart < tokenStart) { + reStr += glob.slice(literalStart, tokenStart); + } + reStr += mapped; + literalStart = i + 1; + } + + if (literalStart < len) { + reStr += literalStart === 0 ? glob : glob.slice(literalStart); + } + + return reStr; +}; diff --git a/package-lock.json b/package-lock.json index 070d92f..c56dcb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "2.5.1", "license": "MIT", "dependencies": { - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" }, "devDependencies": { @@ -5751,12 +5750,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", diff --git a/package.json b/package.json index fec4676..057a515 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "release": "changeset publish" }, "dependencies": { - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" }, "devDependencies": { diff --git a/types/index.d.ts b/types/index.d.ts index 24494a4..3425674 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,4 +1,123 @@ -export = Watchpack; +declare namespace _exports { + export { + WatcherManager, + DirectoryWatcher, + DirectoryWatcherEvents, + FileWatcherEvents, + EventMap, + Watcher, + IgnoredFunction, + Ignored, + WatcherOptions, + WatchOptions, + NormalizedWatchOptions, + EventType, + Entry, + OnlySafeTimeEntry, + ExistenceOnlyTimeEntry, + TimeInfoEntries, + Changes, + Removals, + Aggregated, + WatchMethodOptions, + Times, + WatchpackEvents, + WatchpackExports, + }; +} +declare const _exports: WatchpackExports; +export = _exports; +type WatcherManager = import("./getWatcherManager").WatcherManager; +type DirectoryWatcher = import("./DirectoryWatcher"); +type DirectoryWatcherEvents = + import("./DirectoryWatcher").DirectoryWatcherEvents; +type FileWatcherEvents = import("./DirectoryWatcher").FileWatcherEvents; +type EventMap = Record any>; +type Watcher = import("./DirectoryWatcher").Watcher; +type IgnoredFunction = (item: string) => boolean; +type Ignored = string[] | RegExp | string | IgnoredFunction; +type WatcherOptions = { + /** + * true when need to resolve symlinks and watch symlink and real file, otherwise false + */ + followSymlinks?: boolean | undefined; + /** + * ignore some files from watching (glob pattern or regexp) + */ + ignored?: Ignored | undefined; + /** + * true when need to enable polling mode for watching, otherwise false + */ + poll?: (number | boolean) | undefined; +}; +type WatchOptions = WatcherOptions & { + aggregateTimeout?: number; +}; +type NormalizedWatchOptions = { + /** + * true when need to resolve symlinks and watch symlink and real file, otherwise false + */ + followSymlinks: boolean; + /** + * ignore some files from watching (glob pattern or regexp) + */ + ignored: IgnoredFunction; + /** + * true when need to enable polling mode for watching, otherwise false + */ + poll?: (number | boolean) | undefined; +}; +type EventType = + | `scan (${string})` + | "change" + | "rename" + | `watch ${string}` + | `directory-removed ${string}`; +type Entry = { + safeTime: number; + timestamp: number; + accuracy: number; +}; +type OnlySafeTimeEntry = { + safeTime: number; +}; +type ExistenceOnlyTimeEntry = {}; +type TimeInfoEntries = Map< + string, + Entry | OnlySafeTimeEntry | ExistenceOnlyTimeEntry | null +>; +type Changes = Set; +type Removals = Set; +type Aggregated = { + changes: Changes; + removals: Removals; +}; +type WatchMethodOptions = { + files?: Iterable; + directories?: Iterable; + missing?: Iterable; + startTime?: number; +}; +type Times = Record; +type WatchpackEvents = { + /** + * change event + */ + change: (file: string, mtime: number, type: EventType) => void; + /** + * remove event + */ + remove: (file: string, type: EventType) => void; + /** + * aggregated event + */ + aggregated: (changes: Changes, removals: Removals) => void; +}; +type WatchpackExports = typeof Watchpack & { + util: { + readonly globToRegExp: typeof globToRegExp; + }; +}; /** * @typedef {object} WatchpackEvents * @property {(file: string, mtime: number, type: EventType) => void} change change event @@ -102,32 +221,7 @@ declare class Watchpack extends EventEmitter<{ */ _onRemove(item: string, file: string, type: EventType): void; } -declare namespace Watchpack { - export { - WatcherManager, - DirectoryWatcher, - DirectoryWatcherEvents, - FileWatcherEvents, - EventMap, - Watcher, - IgnoredFunction, - Ignored, - WatcherOptions, - WatchOptions, - NormalizedWatchOptions, - EventType, - Entry, - OnlySafeTimeEntry, - ExistenceOnlyTimeEntry, - TimeInfoEntries, - Changes, - Removals, - Aggregated, - WatchMethodOptions, - Times, - WatchpackEvents, - }; -} +import globToRegExp = require("./util/globToRegExp"); import { EventEmitter } from "events"; declare class WatchpackFileWatcher { /** @@ -173,89 +267,3 @@ declare class WatchpackDirectoryWatcher { update(directories: string | string[]): void; close(): void; } -type WatcherManager = import("./getWatcherManager").WatcherManager; -type DirectoryWatcher = import("./DirectoryWatcher"); -type DirectoryWatcherEvents = - import("./DirectoryWatcher").DirectoryWatcherEvents; -type FileWatcherEvents = import("./DirectoryWatcher").FileWatcherEvents; -type EventMap = Record any>; -type Watcher = import("./DirectoryWatcher").Watcher; -type IgnoredFunction = (item: string) => boolean; -type Ignored = string[] | RegExp | string | IgnoredFunction; -type WatcherOptions = { - /** - * true when need to resolve symlinks and watch symlink and real file, otherwise false - */ - followSymlinks?: boolean | undefined; - /** - * ignore some files from watching (glob pattern or regexp) - */ - ignored?: Ignored | undefined; - /** - * true when need to enable polling mode for watching, otherwise false - */ - poll?: (number | boolean) | undefined; -}; -type WatchOptions = WatcherOptions & { - aggregateTimeout?: number; -}; -type NormalizedWatchOptions = { - /** - * true when need to resolve symlinks and watch symlink and real file, otherwise false - */ - followSymlinks: boolean; - /** - * ignore some files from watching (glob pattern or regexp) - */ - ignored: IgnoredFunction; - /** - * true when need to enable polling mode for watching, otherwise false - */ - poll?: (number | boolean) | undefined; -}; -type EventType = - | `scan (${string})` - | "change" - | "rename" - | `watch ${string}` - | `directory-removed ${string}`; -type Entry = { - safeTime: number; - timestamp: number; - accuracy: number; -}; -type OnlySafeTimeEntry = { - safeTime: number; -}; -type ExistenceOnlyTimeEntry = {}; -type TimeInfoEntries = Map< - string, - Entry | OnlySafeTimeEntry | ExistenceOnlyTimeEntry | null ->; -type Changes = Set; -type Removals = Set; -type Aggregated = { - changes: Changes; - removals: Removals; -}; -type WatchMethodOptions = { - files?: Iterable; - directories?: Iterable; - missing?: Iterable; - startTime?: number; -}; -type Times = Record; -type WatchpackEvents = { - /** - * change event - */ - change: (file: string, mtime: number, type: EventType) => void; - /** - * remove event - */ - remove: (file: string, type: EventType) => void; - /** - * aggregated event - */ - aggregated: (changes: Changes, removals: Removals) => void; -}; diff --git a/types/util/globToRegExp.d.ts b/types/util/globToRegExp.d.ts new file mode 100644 index 0000000..112259d --- /dev/null +++ b/types/util/globToRegExp.d.ts @@ -0,0 +1,2 @@ +declare function _exports(glob: string): string; +export = _exports; diff --git a/types/watchpack.d.ts b/types/watchpack.d.ts index 77bed9a..bff0881 100644 --- a/types/watchpack.d.ts +++ b/types/watchpack.d.ts @@ -1,2 +1,2 @@ -declare const _exports: typeof import("./index"); +declare const _exports: import("./index").WatchpackExports; export = _exports;