src/require/createResolveRequest.js

Summary

Maintainability
D
2 days
Test Coverage
import { sep } from 'path';

import resolve from 'resolve';
import escapeStringRegexp from 'escape-string-regexp';

import { initGetDependencies, initGetDependenciesFromPath } from './manageDependencies';
import createPathRegExp from './createPathRegExp';

export default function createResolveRequest(exports, directory, dependencyContext) {
    const log = require('debug')('roc:core:require'); // eslint-disable-line

    const contextCache = {};
    // Matches a module request, getting the module and a possible scope
    const modulePattern = /^([^\/]*)\/?([^\/]*)/;
    const inProject = initInProject(directory);
    const getCurrentModule = initGetCurrentModule(
        initGetDependencies(dependencyContext),
        initGetDependenciesFromPath(dependencyContext)
    );

    const getContext = (path, fallback) => {
        // Check if we already has the context in the cache and only create it if not
        if (!contextCache[`${path}@@@@@${fallback}`]) {
            contextCache[`${path}@@@@@${fallback}`] = inProject(path) ?
                exports :
                getCurrentModule(path, fallback);
        }

        return contextCache[`${path}@@@@@${fallback}`];
    };

    const requestHelper = (request, context, fallback, identifier) => {
        // If we got an empty request or context we want to let Node handle it
        if (!request || !context) {
            return { completedRequest: request };
        }

        // Provides a way to opt-out of the Roc require system
        if (request.charAt(0) === '#') {
            return { completedRequest: request.substring(1) };
        }

        let rocContext = getContext(context, fallback);
        if (!rocContext && !fallback) {
            return { completedRequest: request };
        } else if (!rocContext) {
            // If all other fails we will resolve in the projects context.
            rocContext = exports;
        }

        log(`(${identifier}) : ${fallback ? '<Fallback> ' : ''}Checking [${request}] for [${context}]`);

        const matches = modulePattern.exec(request);
        const module = matches[1].charAt(0) === '@' ?
            `${matches[1]}/${matches[2]}` :
            matches[1];

        if (!module || !rocContext[module]) {
            return { completedRequest: request };
        }

        return {
            module,
            rocContext: rocContext[module],
        };
    };

    function resolveRequest(identifier = 'No identifier') {
        const resolveCache = {};
        const defaultResolver = (request, context) => resolve.sync(request, { basedir: context });

        const resolveRequestHelper = (request, context, fallback = false, resolver = defaultResolver) => {
            const { completedRequest, module, rocContext } = requestHelper(request, context, fallback, identifier);

            if (completedRequest) {
                return completedRequest;
            }

            const newRequest = rocContext.resolve ?
                rocContext.resolve({
                    extensionContext: rocContext.context,
                    identifier,
                    module,
                    request,
                    requestContext: context,
                }) : resolver(request, rocContext.context);

            log(`(${identifier}) : Found an alias for [${module}] => [${newRequest}]`);

            return newRequest || request;
        };

        return (
            request,
            context,
            {
                fallback = false,
                resolver,
            } = {}
        ) => {
            if (!resolveCache[`${context}@@@@@${request}@@@@@${fallback}`]) {
                resolveCache[`${context}@@@@@${request}@@@@@${fallback}`] =
                    resolveRequestHelper(request, context, fallback, resolver);
            }

            return resolveCache[`${context}@@@@@${request}@@@@@${fallback}`];
        };
    }

    function resolveRequestAsync(identifier = 'No identifier') {
        const resolveCache = {};
        const defaultResolver = (request, context, callback) => resolve(request, { basedir: context }, callback);
        const completeResolve = (callback, request, module) => (error, newRequest) => {
            if (error) {
                return callback(error);
            }

            log(`(${identifier}) : Found an alias for [${module}] => [${newRequest}]`);
            return callback(null, newRequest || request);
        };

        const resolveRequestHelper = (request, context, fallback = false, resolver = defaultResolver, callback) => {
            const { completedRequest, module, rocContext } = requestHelper(request, context, fallback, identifier);

            if (completedRequest) {
                return callback(null, completedRequest);
            }

            if (rocContext.resolve) {
                try {
                    completeResolve(callback, request, module)(null, rocContext.resolve({
                        extensionContext: rocContext.context,
                        identifier,
                        module,
                        request,
                        requestContext: context,
                    }));
                } catch (error) {
                    completeResolve(callback, request, module)(error);
                }
            } else {
                resolver(request, rocContext.context, completeResolve(callback, request, module));
            }
        };

        return (
            request,
            context,
            {
                fallback = false,
                resolver,
            } = {},
            callback
        ) => {
            if (resolveCache[`${context}@@@@@${request}@@@@@${fallback}`]) {
                return callback(null, resolveCache[`${context}@@@@@${request}@@@@@${fallback}`]);
            }

            resolveRequestHelper(request, context, fallback, resolver, (error, newRequest) => {
                if (newRequest) {
                    resolveCache[`${context}@@@@@${request}@@@@@${fallback}`] = newRequest;
                }
                return callback(error, resolveCache[`${context}@@@@@${request}@@@@@${fallback}`]);
            });
        };
    }

    return (identifier, async = false) => {
        if (async) {
            return resolveRequestAsync(identifier);
        }

        return resolveRequest(identifier);
    };
}

function initInProject(directory) {
    const directoryPattern = new RegExp(`^${escapeStringRegexp(directory)}(.*)$`);
    return (path) => {
        const matches = directoryPattern.exec(path);
        if (matches) {
            return matches[0].indexOf(`${sep}node_modules${sep}`) === -1;
        }

        return false;
    };
}

function initGetCurrentModule(getDependencies, getDependenciesFromPath) {
    const fallbackPattern = /roc-.*/;
    const normalPattern = createPathRegExp(`.*node_modules${sep}([^${sep}]*)${sep}?([^${sep}]*)`);
    return (path, fallback) => {
        // Will match against the last roc-* in the path
        if (fallback) {
            const match = path
                .split(sep)
                .reverse()
                .find((name) => fallbackPattern.test(name));

            if (match) {
                return getDependencies(match, 'exports');
            }
        } else {
            const matches = normalPattern.exec(path);

            if (matches) {
                return getDependencies(
                    matches[1].charAt(0) === '@' ?
                        `${matches[1]}/${matches[2]}` :
                        matches[1]
                , 'exports');
            }
        }

        // If we are using "npm link" we will probably end up here
        // This means that we are only running this when doing local development
        return getDependenciesFromPath(path, 'exports');
    };
}