diff --git a/__tests__/index.js b/__tests__/index.js index f78b943..7b25f5a 100644 --- a/__tests__/index.js +++ b/__tests__/index.js @@ -71,7 +71,9 @@ describe("@swc/register", function() { setupRegister(); expect(typeof currentHook).toBe("function"); - expect(currentOptions).toEqual(defaultOptions); + expect(currentOptions.exts).toEqual(defaultOptions.exts); + expect(currentOptions.ignoreNodeModules).toEqual(defaultOptions.ignoreNodeModules); + expect(typeof currentOptions.matcher).toBe("function"); }); test("unregisters hook correctly", () => { @@ -136,4 +138,50 @@ describe("@swc/register", function() { expect(result).toBe('"use strict";\nrequire("assert");\n'); }); + + test("ignore node_modules by default", () => { + setupRegister(); + + expect(currentOptions.matcher('foo.js')).toBe(true); + expect(currentOptions.matcher('node_modules/foo.js')).toBe(false); + expect(currentOptions.matcher('subdir/node_modules/foo.js')).toBe(false); + }); + + test("only compile file under cwd", () => { + setupRegister(); + + expect(currentOptions.matcher('foo.js')).toBe(true); + expect(currentOptions.matcher('/foo.js')).toBe(false); + }); + + test("ignore", () => { + setupRegister({ + ignore: [/ignore/, 'bar/*.js'] + }); + + expect(currentOptions.matcher('foo.js')).toBe(true); + expect(currentOptions.matcher('ignore.js')).toBe(false); + expect(currentOptions.matcher('bar/a.js')).toBe(false); + }); + + test("only", () => { + setupRegister({ + only: [/foo/, (filename) => filename.includes('bar')] + }); + + expect(currentOptions.matcher('foo.js')).toBe(true); + expect(currentOptions.matcher('bar.js')).toBe(true); + expect(currentOptions.matcher('baz.js')).toBe(false); + }); + + test("ignore & only", () => { + setupRegister({ + ignore: [/bar/], + only: [/foo/, (filename) => filename.includes('bar')] + }); + + expect(currentOptions.matcher('foo.js')).toBe(true); + expect(currentOptions.matcher('bar.js')).toBe(false); + expect(currentOptions.matcher('baz.js')).toBe(false); + }); }); diff --git a/package.json b/package.json index a3f1145..1f46f14 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ }, "dependencies": { "lodash.clonedeep": "^4.5.0", - "lodash.escaperegexp": "^4.1.2", "pirates": "^4.0.1", "source-map-support": "^0.5.13" }, diff --git a/src/node.ts b/src/node.ts index d899825..21435cb 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,7 +1,7 @@ import * as swc from "@swc/core"; import fs from "fs"; import deepClone from "lodash.clonedeep"; -import escapeRegExp from "lodash.escaperegexp"; +import { escapeRegExp, pathPatternToRegex } from "./util"; import path from "path"; import { addHook } from "pirates"; import sourceMapSupport from "source-map-support"; @@ -10,15 +10,17 @@ export interface InputOptions extends TransformOptions { extensions?: string[]; } +/** + * Babel has built-in ignore & only support while @swc/core doesn't. So let's make our own! + */ + export interface TransformOptions extends swc.Options { only?: FilePattern; ignore?: FilePattern; } -/** - * TODO: - */ -export type FilePattern = any; +// https://github.com/babel/babel/blob/7e50ee2d823ebc9e50eb3575beb77666214edf8e/packages/babel-core/src/config/validation/options.ts#L201-L202 +export type FilePattern = ReadonlyArray any) | RegExp>; const maps: { [src: string]: string } = {}; let transformOpts: TransformOptions = {}; @@ -89,7 +91,7 @@ function compileHook(code: string, filename: string) { function hookExtensions(exts: readonly string[]) { if (piratesRevert) piratesRevert(); - piratesRevert = addHook(compileHook, { exts: exts as string[], ignoreNodeModules: false }); + piratesRevert = addHook(compileHook, { exts: exts as string[], ignoreNodeModules: false, matcher }); } export function revert() { @@ -128,13 +130,71 @@ export default function register(opts: InputOptions = {}) { // Ignore any node_modules inside the current working directory. new RegExp( "^" + - escapeRegExp(cwd) + - "(?:" + - path.sep + - ".*)?" + - escapeRegExp(path.sep + "node_modules" + path.sep), + escapeRegExp(cwd) + + "(?:" + + path.sep + + ".*)?" + + escapeRegExp(path.sep + "node_modules" + path.sep), "i" ) ]; } } + + +/** + * https://github.com/babel/babel/blob/7acc68a86b70c6aadfef28e10e83d0adb2523807/packages/babel-core/src/config/config-chain.ts + * + * Tests if a filename should be ignored based on "ignore" and "only" options. + */ +function matcher(filename: string, dirname?: string) { + if (!dirname) { + dirname = transformOpts.cwd || path.dirname(filename); + } + return shouldCompile(transformOpts.ignore, transformOpts.only, filename, dirname); +} + +function shouldCompile( + ignore: FilePattern | undefined | null, + only: FilePattern | undefined | null, + filename: string, + dirname: string, +): boolean { + if (ignore && matchPattern(ignore, dirname, filename)) { + return false; + } + if (only && !matchPattern(only, dirname, filename)) { + return false; + } + return true; +} + +/** + * https://github.com/babel/babel/blob/7acc68a86b70c6aadfef28e10e83d0adb2523807/packages/babel-core/src/config/config-chain.ts + * + * Returns result of calling function with filename if pattern is a function. + * Otherwise returns result of matching pattern Regex with filename. + */ +function matchPattern( + patterns: FilePattern, + dirname: string, + pathToTest: string +): boolean { + return patterns.some(pattern => { + if (typeof pattern === "function") { + return Boolean(pattern(pathToTest, { dirname })); + } + + if (typeof pathToTest !== "string") { + throw new Error( + `Configuration contains string/RegExp file pattern, but no filename was provided.`, + ); + } + + if (typeof pattern === "string") { + pattern = pathPatternToRegex(pattern, dirname); + } + return pattern.test(path.resolve(dirname, pathToTest)); + }); +} + diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..b977853 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,59 @@ +import path from "path"; + +export function escapeRegExp(string: string): string { + return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&"); +} + +/** + * Babel + * Released under MIT license + */ +const sep = `\\${path.sep}`; +const endSep = `(?:${sep}|$)`; + +const substitution = `[^${sep}]+`; + +const starPat = `(?:${substitution}${sep})`; +const starPatLast = `(?:${substitution}${endSep})`; + +const starStarPat = `${starPat}*?`; +const starStarPatLast = `${starPat}*?${starPatLast}?`; + +/** + * https://github.com/babel/babel/blob/7acc68a86b70c6aadfef28e10e83d0adb2523807/packages/babel-core/src/config/pattern-to-regex.ts + * + * Implement basic pattern matching that will allow users to do the simple + * tests with * and **. If users want full complex pattern matching, then can + * always use regex matching, or function validation. + */ +export function pathPatternToRegex( + pattern: string, + dirname: string, +): RegExp { + const parts = path.resolve(dirname, pattern).split(path.sep); + + return new RegExp( + [ + "^", + ...parts.map((part, i) => { + const last = i === parts.length - 1; + + // ** matches 0 or more path parts. + if (part === "**") return last ? starStarPatLast : starStarPat; + + // * matches 1 path part. + if (part === "*") return last ? starPatLast : starPat; + + // *.ext matches a wildcard with an extension. + if (part.indexOf("*.") === 0) { + return ( + substitution + escapeRegExp(part.slice(1)) + (last ? endSep : sep) + ); + } + + // Otherwise match the pattern text. + return escapeRegExp(part) + (last ? endSep : sep); + }), + ].join(""), + ); +}