diff --git a/README.md b/README.md index dfe554ae..4fd1e1b6 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ myResolver.resolve({}, lookupStartPath, request, resolveContext, ( | symlinks | true | Whether to resolve symlinks to their symlinked location | | cachePredicate | function() { return true }; | A function which decides whether a request should be cached or not. An object is passed to the function with `path` and `request` properties. | | resolveToContext | false | Resolve to a context instead of a file | +| restrictions | [] | A list of resolve restrictions | | fileSystem | | The file system which should be used | | resolver | undefined | A prepared Resolver to which the plugins are attached | diff --git a/lib/ResolverFactory.js b/lib/ResolverFactory.js index 66a4dfca..1ac476ff 100644 --- a/lib/ResolverFactory.js +++ b/lib/ResolverFactory.js @@ -27,6 +27,7 @@ const ModulesInRootPlugin = require("./ModulesInRootPlugin"); const NextPlugin = require("./NextPlugin"); const ParsePlugin = require("./ParsePlugin"); const PnpPlugin = require("./PnpPlugin"); +const RestrictionsPlugin = require("./RestrictionsPlugin"); const ResultPlugin = require("./ResultPlugin"); const SelfReferencePlugin = require("./SelfReferencePlugin"); const SymlinkPlugin = require("./SymlinkPlugin"); @@ -64,6 +65,7 @@ const UseFilePlugin = require("./UseFilePlugin"); * @property {Plugin[]=} plugins A list of additional resolve plugins which should be applied * @property {PnpApi | null=} pnpApi A PnP API that should be used - null is "never", undefined is "auto" * @property {boolean=} resolveToContext Resolve to a context instead of a file + * @property {(string|RegExp)[]=} restrictions A list of resolve restrictions * @property {boolean=} useSyncFileSystemCalls Use only the sync constiants of the file system calls */ @@ -88,6 +90,7 @@ const UseFilePlugin = require("./UseFilePlugin"); * @property {Plugin[]} plugins * @property {PnpApi | null} pnpApi * @property {boolean} resolveToContext + * @property {Set} restrictions */ /** @@ -181,7 +184,8 @@ function createOptions(options) { mainFiles: new Set(options.mainFiles || ["index"]), plugins: options.plugins || [], pnpApi: processPnpApiOption(options.pnpApi), - resolveToContext: options.resolveToContext || false + resolveToContext: options.resolveToContext || false, + restrictions: new Set(options.restrictions) }; } @@ -211,7 +215,8 @@ exports.createResolver = function(options) { resolveToContext, symlinks, unsafeCache, - resolver: customResolver + resolver: customResolver, + restrictions } = normalizedOptions; const plugins = userPlugins.slice(); @@ -449,6 +454,9 @@ exports.createResolver = function(options) { } // resolved + if (restrictions.size > 0) { + plugins.push(new RestrictionsPlugin(resolver.hooks.resolved, restrictions)); + } plugins.push(new ResultPlugin(resolver.hooks.resolved)); //// RESOLVER //// diff --git a/lib/RestrictionsPlugin.js b/lib/RestrictionsPlugin.js new file mode 100644 index 00000000..1d87a5f4 --- /dev/null +++ b/lib/RestrictionsPlugin.js @@ -0,0 +1,65 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +/** @typedef {import("./Resolver")} Resolver */ +/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */ + +const slashCode = "/".charCodeAt(0); +const backslashCode = "\\".charCodeAt(0); + +const isInside = (path, parent) => { + if (!path.startsWith(parent)) return false; + if (path.length === parent.length) return true; + const charCode = path.charCodeAt(parent.length); + return charCode === slashCode || charCode === backslashCode; +}; + +module.exports = class RestrictionsPlugin { + /** + * @param {string | ResolveStepHook} source source + * @param {Set} restrictions restrictions + */ + constructor(source, restrictions) { + this.source = source; + this.restrictions = restrictions; + } + + /** + * @param {Resolver} resolver the resolver + * @returns {void} + */ + apply(resolver) { + resolver + .getHook(this.source) + .tapAsync("RestrictionsPlugin", (request, resolveContext, callback) => { + if (typeof request.path === "string") { + const path = request.path; + for (const rule of this.restrictions) { + if (typeof rule === "string") { + if (!isInside(path, rule)) { + if (resolveContext.log) { + resolveContext.log( + `${path} is not inside of the restriction ${rule}` + ); + } + return callback(null, null); + } + } else if (!rule.test(path)) { + if (resolveContext.log) { + resolveContext.log( + `${path} doesn't match the restriction ${rule}` + ); + } + return callback(null, null); + } + } + } + + callback(); + }); + } +}; diff --git a/lib/ResultPlugin.js b/lib/ResultPlugin.js index 28d1d4e9..b8496945 100644 --- a/lib/ResultPlugin.js +++ b/lib/ResultPlugin.js @@ -5,12 +5,12 @@ "use strict"; -/** @typedef {import("tapable").AsyncHook} AsyncHook */ /** @typedef {import("./Resolver")} Resolver */ +/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */ module.exports = class ResultPlugin { /** - * @param {AsyncHook} source source + * @param {ResolveStepHook} source source */ constructor(source) { this.source = source; diff --git a/test/fixtures/restrictions/node_modules/pck1/index.css b/test/fixtures/restrictions/node_modules/pck1/index.css new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/restrictions/node_modules/pck1/index.js b/test/fixtures/restrictions/node_modules/pck1/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/restrictions/node_modules/pck1/package.json b/test/fixtures/restrictions/node_modules/pck1/package.json new file mode 100644 index 00000000..14ab704d --- /dev/null +++ b/test/fixtures/restrictions/node_modules/pck1/package.json @@ -0,0 +1,3 @@ +{ + "main": "index.js" +} diff --git a/test/fixtures/restrictions/node_modules/pck2/index.css b/test/fixtures/restrictions/node_modules/pck2/index.css new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/restrictions/node_modules/pck2/module.js b/test/fixtures/restrictions/node_modules/pck2/module.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/restrictions/node_modules/pck2/package.json b/test/fixtures/restrictions/node_modules/pck2/package.json new file mode 100644 index 00000000..ee0eb71b --- /dev/null +++ b/test/fixtures/restrictions/node_modules/pck2/package.json @@ -0,0 +1,5 @@ +{ + "main": "../../../c.js", + "module": "module.js", + "style": "index.css" +} diff --git a/test/restrictions.js b/test/restrictions.js new file mode 100644 index 00000000..da23fda1 --- /dev/null +++ b/test/restrictions.js @@ -0,0 +1,149 @@ +require("should"); +const path = require("path"); +const fs = require("fs"); +const ResolverFactory = require("../lib/ResolverFactory"); +const CachedInputFileSystem = require("../lib/CachedInputFileSystem"); + +const fixture = path.resolve(__dirname, "fixtures", "restrictions"); +const nodeFileSystem = new CachedInputFileSystem(fs, 4000); + +describe("restrictions", () => { + it("should respect RegExp restriction", done => { + const resolver = ResolverFactory.createResolver({ + extensions: [".js"], + fileSystem: nodeFileSystem, + restrictions: [/\.(sass|scss|css)$/] + }); + + resolver.resolve({}, fixture, "pck1", {}, (err, result) => { + if (!err) throw new Error(`expect error, got ${result}`); + err.should.be.instanceof(Error); + done(); + }); + }); + + it("should try to find alternative #1", done => { + const resolver = ResolverFactory.createResolver({ + extensions: [".js", ".css"], + fileSystem: nodeFileSystem, + mainFiles: ["index"], + restrictions: [/\.(sass|scss|css)$/] + }); + + resolver.resolve({}, fixture, "pck1", {}, (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal(path.resolve(fixture, "node_modules/pck1/index.css")); + done(); + }); + }); + + it("should respect string restriction", done => { + const resolver = ResolverFactory.createResolver({ + extensions: [".js"], + fileSystem: nodeFileSystem, + restrictions: [fixture] + }); + + resolver.resolve({}, fixture, "pck2", {}, (err, result) => { + if (!err) throw new Error(`expect error, got ${result}`); + err.should.be.instanceof(Error); + done(); + }); + }); + + it("should try to find alternative #2", done => { + const resolver = ResolverFactory.createResolver({ + extensions: [".js"], + fileSystem: nodeFileSystem, + mainFields: ["main", "style"], + restrictions: [fixture, /\.(sass|scss|css)$/] + }); + + resolver.resolve({}, fixture, "pck2", {}, (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal(path.resolve(fixture, "node_modules/pck2/index.css")); + done(); + }); + }); + + it("should try to find alternative #3", done => { + const resolver = ResolverFactory.createResolver({ + extensions: [".js"], + fileSystem: nodeFileSystem, + mainFields: ["main", "module", "style"], + restrictions: [fixture, /\.(sass|scss|css)$/] + }); + + const log = []; + + resolver.resolve( + {}, + fixture, + "pck2", + { log: log.push.bind(log) }, + (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal( + path.resolve(fixture, "node_modules/pck2/index.css") + ); + log + .map(line => + line + .replace(path.resolve(__dirname, ".."), "...") + .replace(path.resolve(__dirname, ".."), "...") + .replace(/\\/g, "/") + ) + .should.be.eql([ + "resolve 'pck2' in '.../test/fixtures/restrictions'", + " Parsed request is a module", + " using description file: .../package.json (relative path: ./test/fixtures/restrictions)", + " resolve as module", + " looking for modules in .../test/fixtures/restrictions/node_modules", + " single file module", + " using description file: .../package.json (relative path: ./test/fixtures/restrictions/node_modules/pck2)", + " no extension", + " .../test/fixtures/restrictions/node_modules/pck2 is not a file", + " .js", + " .../test/fixtures/restrictions/node_modules/pck2.js doesn't exist", + " existing directory .../test/fixtures/restrictions/node_modules/pck2", + " using description file: .../test/fixtures/restrictions/node_modules/pck2/package.json (relative path: .)", + " using description file: .../package.json (relative path: ./test/fixtures/restrictions/node_modules/pck2)", + " no extension", + " .../test/fixtures/restrictions/node_modules/pck2 is not a file", + " .js", + " .../test/fixtures/restrictions/node_modules/pck2.js doesn't exist", + " as directory", + " existing directory .../test/fixtures/restrictions/node_modules/pck2", + " using description file: .../test/fixtures/restrictions/node_modules/pck2/package.json (relative path: .)", + " use ../../../c.js from main in package.json", + " using description file: .../package.json (relative path: ./test/fixtures/c.js)", + " no extension", + " existing file: .../test/fixtures/c.js", + " .../test/fixtures/c.js is not inside of the restriction .../test/fixtures/restrictions", + " .js", + " .../test/fixtures/c.js.js doesn't exist", + " as directory", + " .../test/fixtures/c.js is not a directory", + " use ./module.js from module in package.json", + " using description file: .../test/fixtures/restrictions/node_modules/pck2/package.json (relative path: ./module.js)", + " no extension", + " existing file: .../test/fixtures/restrictions/node_modules/pck2/module.js", + " .../test/fixtures/restrictions/node_modules/pck2/module.js doesn't match the restriction //.(sass|scss|css)$/", + " .js", + " .../test/fixtures/restrictions/node_modules/pck2/module.js.js doesn't exist", + " as directory", + " .../test/fixtures/restrictions/node_modules/pck2/module.js is not a directory", + " use ./index.css from style in package.json", + " using description file: .../test/fixtures/restrictions/node_modules/pck2/package.json (relative path: ./index.css)", + " no extension", + " existing file: .../test/fixtures/restrictions/node_modules/pck2/index.css", + " reporting result .../test/fixtures/restrictions/node_modules/pck2/index.css" + ]); + done(); + } + ); + }); +}); diff --git a/types.d.ts b/types.d.ts index 5aae597b..2b4a814d 100644 --- a/types.d.ts +++ b/types.d.ts @@ -134,6 +134,7 @@ declare interface ResolveOptions { | ((this: Resolver, arg1: Resolver) => void))[]; pnpApi: null | PnpApiImpl; resolveToContext: boolean; + restrictions: Set; } declare interface ResolveRequest { path: string | false; @@ -335,6 +336,11 @@ declare interface UserResolveOptions { */ resolveToContext?: undefined | boolean; + /** + * A list of resolve restrictions + */ + restrictions?: undefined | (string | RegExp)[]; + /** * Use only the sync constiants of the file system calls */