Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
12 changes: 10 additions & 2 deletions lib/ResolverFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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
*/

Expand All @@ -88,6 +90,7 @@ const UseFilePlugin = require("./UseFilePlugin");
* @property {Plugin[]} plugins
* @property {PnpApi | null} pnpApi
* @property {boolean} resolveToContext
* @property {Set<string|RegExp>} restrictions
*/

/**
Expand Down Expand Up @@ -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)
};
}

Expand Down Expand Up @@ -211,7 +215,8 @@ exports.createResolver = function(options) {
resolveToContext,
symlinks,
unsafeCache,
resolver: customResolver
resolver: customResolver,
restrictions
} = normalizedOptions;

const plugins = userPlugins.slice();
Expand Down Expand Up @@ -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 ////
Expand Down
65 changes: 65 additions & 0 deletions lib/RestrictionsPlugin.js
Original file line number Diff line number Diff line change
@@ -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<string | RegExp>} 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();
});
}
};
4 changes: 2 additions & 2 deletions lib/ResultPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Empty file.
Empty file.
3 changes: 3 additions & 0 deletions test/fixtures/restrictions/node_modules/pck1/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
Empty file.
5 changes: 5 additions & 0 deletions test/fixtures/restrictions/node_modules/pck2/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

149 changes: 149 additions & 0 deletions test/restrictions.js
Original file line number Diff line number Diff line change
@@ -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();
}
);
});
});
6 changes: 6 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ declare interface ResolveOptions {
| ((this: Resolver, arg1: Resolver) => void))[];
pnpApi: null | PnpApiImpl;
resolveToContext: boolean;
restrictions: Set<string | RegExp>;
}
declare interface ResolveRequest {
path: string | false;
Expand Down Expand Up @@ -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
*/
Expand Down