react-saasify-chrisvxd
Version:
React components for Saasify web clients.
449 lines (376 loc) • 13 kB
JavaScript
const builtins = require('./builtins');
const nodeBuiltins = require('node-libs-browser');
const path = require('path');
const {isGlob} = require('./utils/glob');
const fs = require('@parcel/fs');
const micromatch = require('micromatch');
const getModuleParts = require('./utils/getModuleParts');
const EMPTY_SHIM = require.resolve('./builtins/_empty');
/**
* This resolver implements a modified version of the node_modules resolution algorithm:
* https://nodejs.org/api/modules.html#modules_all_together
*
* In addition to the standard algorithm, Parcel supports:
* - All file extensions supported by Parcel.
* - Glob file paths
* - Absolute paths (e.g. /foo) resolved relative to the project root.
* - Tilde paths (e.g. ~/foo) resolved relative to the nearest module root in node_modules.
* - The package.json module, jsnext:main, and browser field as replacements for package.main.
* - The package.json browser and alias fields as an alias map within a local module.
* - The package.json alias field in the root package for global aliases across all modules.
*/
class Resolver {
constructor(options = {}) {
this.options = options;
this.cache = new Map();
this.packageCache = new Map();
this.rootPackage = null;
}
async resolve(input, parent) {
let filename = input;
// Check the cache first
let key = this.getCacheKey(filename, parent);
if (this.cache.has(key)) {
return this.cache.get(key);
}
// Check if this is a glob
if (isGlob(filename)) {
return {path: path.resolve(path.dirname(parent), filename)};
}
// Get file extensions to search
let extensions = Array.isArray(this.options.extensions)
? this.options.extensions.slice()
: Object.keys(this.options.extensions);
if (parent) {
// parent's extension given high priority
const parentExt = path.extname(parent);
extensions = [parentExt, ...extensions.filter(ext => ext !== parentExt)];
}
extensions.unshift('');
// Resolve the module directory or local file path
let module = await this.resolveModule(filename, parent);
let resolved;
if (module.moduleDir) {
resolved = await this.loadNodeModules(module, extensions);
} else if (module.filePath) {
resolved = await this.loadRelative(module.filePath, extensions);
}
if (!resolved) {
let dir = parent ? path.dirname(parent) : process.cwd();
let err = new Error(`Cannot find module '${input}' from '${dir}'`);
err.code = 'MODULE_NOT_FOUND';
throw err;
}
this.cache.set(key, resolved);
return resolved;
}
async resolveModule(filename, parent) {
let dir = parent ? path.dirname(parent) : process.cwd();
// If this isn't the entrypoint, resolve the input file to an absolute path
if (parent) {
filename = this.resolveFilename(filename, dir);
}
// Resolve aliases in the parent module for this file.
filename = await this.loadAlias(filename, dir);
// Return just the file path if this is a file, not in node_modules
if (path.isAbsolute(filename)) {
return {
filePath: filename
};
}
// Resolve the module in node_modules
let resolved;
try {
resolved = await this.findNodeModulePath(filename, dir);
} catch (err) {
// ignore
}
// If we couldn't resolve the node_modules path, just return the module name info
if (!resolved) {
let parts = getModuleParts(filename);
resolved = {
moduleName: parts[0],
subPath: parts[1]
};
}
return resolved;
}
getCacheKey(filename, parent) {
return (parent ? path.dirname(parent) : '') + ':' + filename;
}
resolveFilename(filename, dir) {
switch (filename[0]) {
case '/':
// Absolute path. Resolve relative to project root.
return path.resolve(this.options.rootDir, filename.slice(1));
case '~':
// Tilde path. Resolve relative to nearest node_modules directory,
// or the project root - whichever comes first.
while (
dir !== this.options.rootDir &&
path.basename(path.dirname(dir)) !== 'node_modules'
) {
dir = path.dirname(dir);
if (dir === path.dirname(dir)) {
dir = this.options.rootDir;
break;
}
}
return path.join(dir, filename.slice(1));
case '.':
// Relative path.
return path.resolve(dir, filename);
default:
// Module
return filename;
}
}
async loadRelative(filename, extensions) {
// Find a package.json file in the current package.
let pkg = await this.findPackage(path.dirname(filename));
// First try as a file, then as a directory.
return (
(await this.loadAsFile(filename, extensions, pkg)) ||
(await this.loadDirectory(filename, extensions, pkg)) // eslint-disable-line no-return-await
);
}
async findNodeModulePath(filename, dir) {
if (builtins[filename]) {
if (this.options.target === 'node' && filename in nodeBuiltins) {
throw new Error('Cannot resolve builtin module for node target');
}
return {filePath: builtins[filename]};
}
let parts = getModuleParts(filename);
let root = path.parse(dir).root;
while (dir !== root) {
// Skip node_modules directories
if (path.basename(dir) === 'node_modules') {
dir = path.dirname(dir);
}
try {
// First, check if the module directory exists. This prevents a lot of unnecessary checks later.
let moduleDir = path.join(dir, 'node_modules', parts[0]);
let stats = await fs.stat(moduleDir);
if (stats.isDirectory()) {
return {
moduleName: parts[0],
subPath: parts[1],
moduleDir: moduleDir,
filePath: path.join(dir, 'node_modules', filename)
};
}
} catch (err) {
// ignore
}
// Move up a directory
dir = path.dirname(dir);
}
}
async loadNodeModules(module, extensions) {
try {
// If a module was specified as a module sub-path (e.g. some-module/some/path),
// it is likely a file. Try loading it as a file first.
if (module.subPath) {
let pkg = await this.readPackage(module.moduleDir);
let res = await this.loadAsFile(module.filePath, extensions, pkg);
if (res) {
return res;
}
}
// Otherwise, load as a directory.
return await this.loadDirectory(module.filePath, extensions);
} catch (e) {
// ignore
}
}
async isFile(file) {
try {
let stat = await fs.stat(file);
return stat.isFile() || stat.isFIFO();
} catch (err) {
return false;
}
}
async loadDirectory(dir, extensions, pkg) {
try {
pkg = await this.readPackage(dir);
// Get a list of possible package entry points.
let entries = this.getPackageEntries(pkg);
for (let file of entries) {
// First try loading package.main as a file, then try as a directory.
const res =
(await this.loadAsFile(file, extensions, pkg)) ||
(await this.loadDirectory(file, extensions, pkg));
if (res) {
return res;
}
}
} catch (err) {
// ignore
}
// Fall back to an index file inside the directory.
return this.loadAsFile(path.join(dir, 'index'), extensions, pkg);
}
async readPackage(dir) {
let file = path.join(dir, 'package.json');
if (this.packageCache.has(file)) {
return this.packageCache.get(file);
}
let json = await fs.readFile(file, 'utf8');
let pkg = JSON.parse(json);
pkg.pkgfile = file;
pkg.pkgdir = dir;
// If the package has a `source` field, check if it is behind a symlink.
// If so, we treat the module as source code rather than a pre-compiled module.
if (pkg.source) {
let realpath = await fs.realpath(file);
if (realpath === file) {
delete pkg.source;
}
}
this.packageCache.set(file, pkg);
return pkg;
}
getBrowserField(pkg) {
let target = this.options.target || 'browser';
return target === 'browser' ? pkg.browser : null;
}
getPackageEntries(pkg) {
let browser = this.getBrowserField(pkg);
if (browser && typeof browser === 'object' && browser[pkg.name]) {
browser = browser[pkg.name];
}
// libraries like d3.js specifies node.js specific files in the "main" which breaks the build
// we use the "browser" or "module" field to get the full dependency tree if available.
// If this is a linked module with a `source` field, use that as the entry point.
return [pkg.source, browser, pkg.module, pkg.main]
.filter(entry => typeof entry === 'string')
.map(main => {
// Default to index file if no main field find
if (!main || main === '.' || main === './') {
main = 'index';
}
return path.resolve(pkg.pkgdir, main);
});
}
async loadAsFile(file, extensions, pkg) {
// Try all supported extensions
for (let f of this.expandFile(file, extensions, pkg)) {
if (await this.isFile(f)) {
return {path: f, pkg};
}
}
}
expandFile(file, extensions, pkg, expandAliases = true) {
// Expand extensions and aliases
let res = [];
for (let ext of extensions) {
let f = file + ext;
if (expandAliases) {
let alias = this.resolveAliases(file + ext, pkg);
if (alias !== f) {
res = res.concat(this.expandFile(alias, extensions, pkg, false));
}
}
res.push(f);
}
return res;
}
resolveAliases(filename, pkg) {
// First resolve local package aliases, then project global ones.
return this.resolvePackageAliases(
this.resolvePackageAliases(filename, pkg),
this.rootPackage
);
}
resolvePackageAliases(filename, pkg) {
if (!pkg) {
return filename;
}
// Resolve aliases in the package.source, package.alias, and package.browser fields.
return (
this.getAlias(filename, pkg.pkgdir, pkg.source) ||
this.getAlias(filename, pkg.pkgdir, pkg.alias) ||
this.getAlias(filename, pkg.pkgdir, this.getBrowserField(pkg)) ||
filename
);
}
getAlias(filename, dir, aliases) {
if (!filename || !aliases || typeof aliases !== 'object') {
return null;
}
let alias;
// If filename is an absolute path, get one relative to the package.json directory.
if (path.isAbsolute(filename)) {
filename = path.relative(dir, filename);
if (filename[0] !== '.') {
filename = './' + filename;
}
alias = this.lookupAlias(aliases, filename, dir);
} else {
// It is a node_module. First try the entire filename as a key.
alias = this.lookupAlias(aliases, filename, dir);
if (alias == null) {
// If it didn't match, try only the module name.
let parts = getModuleParts(filename);
alias = this.lookupAlias(aliases, parts[0], dir);
if (typeof alias === 'string') {
// Append the filename back onto the aliased module.
alias = path.join(alias, ...parts.slice(1));
}
}
}
// If the alias is set to `false`, return an empty file.
if (alias === false) {
return EMPTY_SHIM;
}
return alias;
}
lookupAlias(aliases, filename, dir) {
// First, try looking up the exact filename
let alias = aliases[filename];
if (alias == null) {
// Otherwise, try replacing glob keys
for (let key in aliases) {
if (isGlob(key)) {
let re = micromatch.makeRe(key, {capture: true});
if (re.test(filename)) {
alias = filename.replace(re, aliases[key]);
break;
}
}
}
// Or try a lookup replacing backslash characters with forward slash
if (alias == null && ~filename.indexOf('\\')) {
alias = aliases[filename.replace(/\\/g, '/')];
}
}
if (typeof alias === 'string') {
return this.resolveFilename(alias, dir);
}
return alias;
}
async findPackage(dir) {
// Find the nearest package.json file within the current node_modules folder
let root = path.parse(dir).root;
while (dir !== root && path.basename(dir) !== 'node_modules') {
try {
return await this.readPackage(dir);
} catch (err) {
// ignore
}
dir = path.dirname(dir);
}
}
async loadAlias(filename, dir) {
// Load the root project's package.json file if we haven't already
if (!this.rootPackage) {
this.rootPackage = await this.findPackage(this.options.rootDir);
}
// Load the local package, and resolve aliases
let pkg = await this.findPackage(dir);
return this.resolveAliases(filename, pkg);
}
}
module.exports = Resolver;