|
<?php |
|
|
|
use League\Flysystem\FilesystemInterface; |
|
use Illuminate\Support\Str; |
|
|
|
class NodePathResolver |
|
{ |
|
/** |
|
* @var FilesystemInterface |
|
*/ |
|
protected $fs; |
|
|
|
/** |
|
* @var string |
|
*/ |
|
protected $contextDir; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
protected $additionalPaths; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
protected $extensions; |
|
|
|
const IS_WINDOWS = DIRECTORY_SEPARATOR === '\\'; |
|
const DIRECTORY_SEPARATOR = self::IS_WINDOWS ? '/\\' : '/'; |
|
const REGEX_DIRECTORY_SEPARATOR = self::IS_WINDOWS ? '[/\\\\]' : '[/]'; |
|
const REGEX_ABSOLUTE_PATH = |
|
'@^(' . |
|
(self::IS_WINDOWS |
|
? '[a-zA-Z]:' . self::DIRECTORY_SEPARATOR . '|' |
|
: '' |
|
) . |
|
self::REGEX_DIRECTORY_SEPARATOR . |
|
')@'; |
|
const REGEX_RELATIVE_PATH = |
|
'@^' . |
|
'\\.\\.?' . |
|
'(' . self::REGEX_DIRECTORY_SEPARATOR . '|$)' . |
|
'@'; |
|
|
|
/** |
|
* @param FilesystemInterface $fs The filesystem to operate on |
|
* @param string $contextDir The directory to start resolving at |
|
* @param array $options Optional settings to use: |
|
* extensions => An array of additional extensions to try (besides .js and .json) |
|
* paths => An array of additional module paths to search before the node_modules chain |
|
*/ |
|
public function __construct(FilesystemInterface $fs, string $contextDir, array $options = []) |
|
{ |
|
$this->fs = $fs; |
|
$this->contextDir = $this->normalizePath($contextDir); |
|
$this->additionalPaths = $options['paths'] ?? []; |
|
$this->extensions = array_merge(['js', 'json'], $options['extensions'] ?? []); |
|
} |
|
|
|
/** |
|
* Resolve a descriptor relative to the context directory |
|
* |
|
* @param string $descriptor The descriptor to resolve |
|
* @return string |
|
* |
|
* @throws \InvalidArgumentException When descriptor cannot be resolved |
|
*/ |
|
public function resolve(string $descriptor): string |
|
{ |
|
$root = $this->contextDir . '/'; |
|
if ($this->isAbsolutePath($descriptor)) { |
|
$root = static::IS_WINDOWS ? '' : '/'; |
|
} |
|
|
|
if ($this->isAbsolutePath($descriptor) || $this->isRelativePath($descriptor)) { |
|
$path = $this->loadAsFile($root . $descriptor); |
|
if (!is_null($path)) { |
|
return $path; |
|
} |
|
|
|
$path = $this->loadAsDirectory($root . $descriptor); |
|
if (!is_null($path)) { |
|
return $path; |
|
} |
|
} |
|
|
|
$path = $this->loadFromModules($descriptor, $root); |
|
if (!is_null($path)) { |
|
return $path; |
|
} |
|
|
|
throw new \InvalidArgumentException(sprintf( |
|
'Could not resolve descriptor "%s" in %s', |
|
$descriptor, |
|
$this->contextDir |
|
)); |
|
} |
|
|
|
/** |
|
* Check if a path exists and is a directory |
|
* |
|
* @param string $path The path to check |
|
* @return boolean |
|
*/ |
|
protected function existsAndIsFile(string $path): bool |
|
{ |
|
return $this->fs->has($path) && $this->fs->getMetadata($path)['type'] === 'file'; |
|
} |
|
|
|
/** |
|
* Check if a path exists and is a file |
|
* |
|
* @param string $path The path to check |
|
* @return boolean |
|
*/ |
|
protected function existsAndIsDir(string $path): bool |
|
{ |
|
return $this->fs->has($path) && $this->fs->getMetadata($path)['type'] === 'dir'; |
|
} |
|
|
|
/** |
|
* Check if a descriptor points to an absolute path |
|
* |
|
* @param string $descriptor The descriptor to check |
|
* @return boolean |
|
*/ |
|
protected function isAbsolutePath(string $descriptor): bool |
|
{ |
|
return preg_match(static::REGEX_ABSOLUTE_PATH, $descriptor); |
|
} |
|
|
|
/** |
|
* Check if a descriptor points to a relative path |
|
* |
|
* @param string $descriptor The descriptor to check |
|
* @return boolean |
|
*/ |
|
protected function isRelativePath(string $descriptor): bool |
|
{ |
|
return preg_match(static::REGEX_RELATIVE_PATH, $descriptor); |
|
} |
|
|
|
/** |
|
* Load the given path as file |
|
* |
|
* @param string $path The path to load |
|
* @param boolean $allowSelf If the path itself can be returned if it exists |
|
* @return string|null |
|
*/ |
|
protected function loadAsFile(string $path, bool $allowSelf = true): ?string |
|
{ |
|
if ($allowSelf) { |
|
if ($this->existsAndIsFile($path)) { |
|
return $path; |
|
} |
|
} |
|
|
|
foreach ($this->extensions as $extension) { |
|
if ($this->existsAndIsFile($path . '.' . $extension)) { |
|
return $path . '.' . $extension; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/** |
|
* Load index file from folder |
|
* |
|
* @param string $directory The directory to check |
|
* @return string|null |
|
*/ |
|
protected function loadIndex(string $directory): ?string |
|
{ |
|
return $this->loadAsFile($directory . '/index', false); |
|
} |
|
|
|
/** |
|
* Find the entry point of a directory |
|
* |
|
* @param string $directory The path to the directory |
|
* @return string|null |
|
*/ |
|
protected function loadAsDirectory(string $directory): ?string |
|
{ |
|
$packageJson = $directory . '/package.json'; |
|
if ($this->existsAndIsFile($packageJson)) { |
|
$packageData = json_decode($this->fs->read($packageJson)); |
|
if (json_last_error() !== JSON_ERROR_NONE) { |
|
throw new \UnexpectedValueException(sprintf( |
|
'Could not parse package.json in "%s", error: %s', |
|
$directory, |
|
json_last_error_msg() |
|
)); |
|
} |
|
|
|
$mainPath = $this->normalizePath( |
|
$directory . '/' . |
|
(isset($packageData->main) ? $packageData->main : '') |
|
); |
|
|
|
$path = $this->loadAsFile($mainPath); |
|
if (!is_null($path)) { |
|
return $path; |
|
} |
|
|
|
$path = $this->loadIndex($mainPath); |
|
if (!is_null($path)) { |
|
return $path; |
|
} |
|
} |
|
|
|
$path = $this->loadIndex($directory); |
|
if (!is_null($path)) { |
|
return $path; |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/** |
|
* Load descriptor from node_modules |
|
* |
|
* @param string $descriptor The descriptor to load |
|
* @param string $startDirectory The path to start looking for node_modules |
|
* @return string|null |
|
*/ |
|
protected function loadFromModules(string $descriptor, string $startDirectory): ?string |
|
{ |
|
$moduleDirs = $this->getModuleDirs($startDirectory); |
|
|
|
foreach ($moduleDirs as $moduleDir) { |
|
$path = $this->loadAsFile($moduleDir . '/' . $descriptor); |
|
if (!is_null($path)) { |
|
return $path; |
|
} |
|
|
|
$path = $this->loadAsDirectory($moduleDir . '/' . $descriptor); |
|
if (!is_null($path)) { |
|
return $path; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/** |
|
* The module directories to search |
|
* |
|
* @return array |
|
*/ |
|
protected function getModuleDirs(string $startDirectory): array |
|
{ |
|
$dirs = $this->additionalPaths; |
|
$currentContextDir = $startDirectory; |
|
|
|
do { |
|
$currentModulesDir = $currentContextDir . '/node_modules'; |
|
if ($this->fs->has($currentModulesDir)) { |
|
$dirs[] = $currentModulesDir; |
|
} |
|
|
|
$currentContextDir = dirname($currentContextDir); |
|
} while ($currentContextDir !== dirname($currentContextDir)); |
|
|
|
return $dirs; |
|
} |
|
|
|
/** |
|
* Normalize a path (remove ./ and ../) |
|
* |
|
* @param string $path The path to normalize |
|
* @return string |
|
*/ |
|
protected function normalizePath(string $path): string |
|
{ |
|
$root = ($path[0] === '/') ? '/' : ''; |
|
|
|
$segments = preg_split( |
|
static::REGEX_DIRECTORY_SEPARATOR, |
|
trim($path, static::DIRECTORY_SEPARATOR) |
|
); |
|
$ret = []; |
|
foreach ($segments as $segment) { |
|
if (($segment == '.') || Str::length($segment) === 0) { |
|
continue; |
|
} |
|
if ($segment == '..') { |
|
array_pop($ret); |
|
} else { |
|
array_push($ret, $segment); |
|
} |
|
} |
|
|
|
return $root . implode('/', $ret); |
|
} |
|
} |