README.md 0000666 00000007131 13436753331 0006041 0 ustar 00 [![Latest Stable Version](https://poser.pugx.org/mouf/nodejs-installer/v/stable.svg)](https://packagist.org/packages/mouf/nodejs-installer)
[![Latest Unstable Version](https://poser.pugx.org/mouf/nodejs-installer/v/unstable.svg)](https://packagist.org/packages/mouf/nodejs-installer)
[![License](https://poser.pugx.org/mouf/nodejs-installer/license.svg)](https://packagist.org/packages/mouf/nodejs-installer)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/thecodingmachine/nodejs-installer/badges/quality-score.png?b=1.0)](https://scrutinizer-ci.com/g/thecodingmachine/nodejs-installer/?branch=1.0)
NodeJS installer for Composer
=============================
This is an installer that will download NodeJS and NPM and install them in your Composer dependencies.
Installation is skipped if NodeJS is already available on your machine.
Why?
----
NodeJS is increasingly becoming a part of the tool-chain of modern web developers. Tools like Bower, Grunt, Gulp... are
used everyday to build applications. For the PHP developer, this means PHP projects have build dependencies on NodeJS
or Bower / NPM packages. The NodeJS-installer attempts to bridge the gap between NodeJS and PHP by making NodeJS easily
installable as a Composer dependency.
Building on this package, other packages like [koala-framework/composer-extra-assets](https://github.com/koala-framework/composer-extra-assets)
can be used to automatically fetch Bower / NPM packages, run Gulp / Grunt tasks, etc...
How does it work?
-----------------
Simply include this package in your `composer.json` requirements:
```json
{
"require": {
"mouf/nodejs-installer": "~1.0"
}
}
```
By default, if NodeJS is not available on your computer, it will be downloaded and installed in *vendor/nodejs/nodejs*.
You should access NodeJS and NPM using the scripts created into the *vendor/bin* directory:
- *vendor/bin/node* (*vendor/bin/node.bat* on Windows)
- *vendor/bin/npm* (*vendor/bin/npm.bat* on Windows)
Options
-------
A number of options are available to customize NodeJS installation:
```json
{
"require": {
"mouf/nodejs-installer": "~1.0"
},
"extra": {
"mouf": {
"nodejs": {
"version": "~0.12",
"targetDir": "vendor/nodejs/nodejs",
"forceLocal": false
}
}
}
}
```
Available options:
- **version**: This is the version number of NodeJS that will be downloaded and installed.
You can specify version constraints in the usual Composer format (for instance "~0.12" or ">0.11").
_Default value: *_ The latest stable version of NodeJS is installed by default.
- **targetDir**: The target directory NodeJS will be installed in. Relative to project root.
This option is only available in the root package.
*Default value: vendor/nodejs/nodejs*
- **forceLocal** (boolean): If set to true, NodeJS will always be downloaded and installed locally, even if NodeJS
is already available on your computer.
This option is only available in the root package.
*Default value: false*
- **includeBinInPath** (boolean): After the plugin is run in Composer, the *vendor/bin* directory can optionally be
added to the PATH. This is useful if other plugins rely on "node" or "npm" being available globally on the
computer. Using this option, these other plugins will automatically find the node/npm version that has been
downloaded. Please note that the PATH is only set for the duration of the Composer script. Your global environment
is not impacted by this option.
This option is only available in the root package.
*Default value: false*
src/Environment.php 0000666 00000002612 13436753331 0010365 0 ustar 00 io = $io;
$this->rfs = new RemoteFilesystem($io);
}
public function getList()
{
// Let's download the content of HTML page https://nodejs.org/dist/
$html = $this->rfs->getContents(parse_url(self::NODEJS_DIST_URL, PHP_URL_HOST), self::NODEJS_DIST_URL, false);
// Now, let's parse it!
$matches = array();
preg_match_all("$>v([0-9]*\\.[0-9]*\\.[0-9]*)/<$", $html, $matches);
if (!isset($matches[1])) {
throw new NodeJsInstallerException("Error while querying ".self::NODEJS_DIST_URL.". Unable to find NodeJS
versions on this page.");
}
return $matches[1];
}
}
src/NodeJsPlugin.php 0000666 00000021176 13436753331 0010430 0 ustar 00 composer = $composer;
$this->io = $io;
}
/**
* Let's register the harmony dependencies update events.
*
* @return array
*/
public static function getSubscribedEvents()
{
return array(
ScriptEvents::POST_INSTALL_CMD => array(
array('onPostUpdateInstall', 1),
),
ScriptEvents::POST_UPDATE_CMD => array(
array('onPostUpdateInstall', 1),
),
);
}
/**
* Script callback; Acted on after install or update.
*/
public function onPostUpdateInstall(Event $event)
{
$settings = array(
'targetDir' => 'vendor/nodejs/nodejs',
'forceLocal' => false,
'includeBinInPath' => false,
);
$extra = $event->getComposer()->getPackage()->getExtra();
if (isset($extra['mouf']['nodejs'])) {
$rootSettings = $extra['mouf']['nodejs'];
$settings = array_merge($settings, $rootSettings);
$settings['targetDir'] = trim($settings['targetDir'], '/\\');
}
$binDir = $event->getComposer()->getConfig()->get('bin-dir');
if (!class_exists(__NAMESPACE__.'\\NodeJsVersionMatcher')) {
//The package is being uninstalled
$this->onUninstall($binDir, $settings['targetDir']);
return;
}
$nodeJsVersionMatcher = new NodeJsVersionMatcher();
$versionConstraint = $this->getMergedVersionConstraint();
$this->verboseLog("NodeJS installer:");
$this->verboseLog(" - Requested version: ".$versionConstraint);
$nodeJsInstaller = new NodeJsInstaller($this->io);
$isLocal = false;
if ($settings['forceLocal']) {
$this->verboseLog(" - Forcing local NodeJS install.");
$this->installLocalVersion($binDir, $nodeJsInstaller, $versionConstraint, $settings['targetDir']);
$isLocal = true;
} else {
$globalVersion = $nodeJsInstaller->getNodeJsGlobalInstallVersion();
if ($globalVersion !== null) {
$this->verboseLog(" - Global NodeJS install found: v".$globalVersion);
$npmPath = $nodeJsInstaller->getGlobalInstallPath('npm');
if (!$npmPath) {
$this->verboseLog(" - No NPM install found");
$this->installLocalVersion($binDir, $nodeJsInstaller, $versionConstraint, $settings['targetDir']);
$isLocal = true;
} elseif (!$nodeJsVersionMatcher->isVersionMatching($globalVersion, $versionConstraint)) {
$this->installLocalVersion($binDir, $nodeJsInstaller, $versionConstraint, $settings['targetDir']);
$isLocal = true;
} else {
$this->verboseLog(" - Global NodeJS install matches constraint ".$versionConstraint);
}
} else {
$this->verboseLog(" - No global NodeJS install found");
$this->installLocalVersion($binDir, $nodeJsInstaller, $versionConstraint, $settings['targetDir']);
$isLocal = true;
}
}
// Now, let's create the bin scripts that start node and NPM
$nodeJsInstaller->createBinScripts($binDir, $settings['targetDir'], $isLocal);
// Finally, let's register vendor/bin in the PATH.
if ($settings['includeBinInPath']) {
$nodeJsInstaller->registerPath($binDir);
}
}
/**
* Writes message only in verbose mode.
* @param string $message
*/
private function verboseLog($message)
{
if ($this->io->isVerbose()) {
$this->io->write($message);
}
}
/**
* Checks local NodeJS version, performs install if needed.
*
* @param string $binDir
* @param NodeJsInstaller $nodeJsInstaller
* @param string $versionConstraint
* @param string $targetDir
* @throws NodeJsInstallerException
*/
private function installLocalVersion($binDir, NodeJsInstaller $nodeJsInstaller, $versionConstraint, $targetDir)
{
$nodeJsVersionMatcher = new NodeJsVersionMatcher();
$localVersion = $nodeJsInstaller->getNodeJsLocalInstallVersion($binDir);
if ($localVersion !== null) {
$this->verboseLog(" - Local NodeJS install found: v".$localVersion);
if (!$nodeJsVersionMatcher->isVersionMatching($localVersion, $versionConstraint)) {
$this->installBestPossibleLocalVersion($nodeJsInstaller, $versionConstraint, $targetDir);
} else {
// Question: should we update to the latest version? Should we have a nodejs.lock file???
$this->verboseLog(" - Local NodeJS install matches constraint ".$versionConstraint);
}
} else {
$this->verboseLog(" - No local NodeJS install found");
$this->installBestPossibleLocalVersion($nodeJsInstaller, $versionConstraint, $targetDir);
}
}
/**
* Installs locally the best possible NodeJS version matching $versionConstraint
*
* @param NodeJsInstaller $nodeJsInstaller
* @param string $versionConstraint
* @param string $targetDir
* @throws NodeJsInstallerException
*/
private function installBestPossibleLocalVersion(NodeJsInstaller $nodeJsInstaller, $versionConstraint, $targetDir)
{
$nodeJsVersionsLister = new NodeJsVersionsLister($this->io);
$allNodeJsVersions = $nodeJsVersionsLister->getList();
$nodeJsVersionMatcher = new NodeJsVersionMatcher();
$bestPossibleVersion = $nodeJsVersionMatcher->findBestMatchingVersion($allNodeJsVersions, $versionConstraint);
if ($bestPossibleVersion === null) {
throw new NodeJsInstallerNodeVersionException("No NodeJS version could be found for constraint '".$versionConstraint."'");
}
$nodeJsInstaller->install($bestPossibleVersion, $targetDir);
}
/**
* Gets the version constraint from all included packages and merges it into one constraint.
*/
private function getMergedVersionConstraint()
{
$packagesList = $this->composer->getRepositoryManager()->getLocalRepository()
->getCanonicalPackages();
$packagesList[] = $this->composer->getPackage();
$versions = array();
foreach ($packagesList as $package) {
if ($package instanceof AliasPackage) {
$package = $package->getAliasOf();
}
if ($package instanceof CompletePackage) {
$extra = $package->getExtra();
if (isset($extra['mouf']['nodejs']['version'])) {
$versions[] = $extra['mouf']['nodejs']['version'];
}
}
}
if (!empty($versions)) {
return implode(", ", $versions);
} else {
return "*";
}
}
/**
* Uninstalls NodeJS.
* Note: other classes cannot be loaded here since the package has already been removed.
*/
private function onUninstall($binDir, $targetDir)
{
$fileSystem = new Filesystem();
if (file_exists($targetDir)) {
$this->verboseLog("Removing NodeJS local install");
// Let's remove target directory
$fileSystem->remove($targetDir);
$vendorNodeDir = dirname($targetDir);
if ($fileSystem->isDirEmpty($vendorNodeDir)) {
$fileSystem->remove($vendorNodeDir);
}
}
// Now, let's remove the links
$this->verboseLog("Removing NodeJS and NPM links from Composer bin directory");
foreach (array("node", "npm", "node.bat", "npm.bat") as $file) {
$realFile = $binDir.DIRECTORY_SEPARATOR.$file;
if (file_exists($realFile)) {
$fileSystem->remove($realFile);
}
}
}
}
src/NodeJsInstaller.php 0000666 00000034440 13436753331 0011125 0 ustar 00 io = $io;
$this->rfs = new RemoteFilesystem($io);
}
/**
* Checks if NodeJS is installed globally.
* If yes, will return the version number.
* If no, will return null.
*
* Note: trailing "v" will be removed from version string.
*
* @return null|string
*/
public function getNodeJsGlobalInstallVersion()
{
$returnCode = 0;
$output = "";
ob_start();
$version = exec("nodejs -v 2>&1", $output, $returnCode);
ob_end_clean();
if ($returnCode !== 0) {
ob_start();
$version = exec("node -v 2>&1", $output, $returnCode);
ob_end_clean();
if ($returnCode !== 0) {
return;
}
}
return ltrim($version, "v");
}
/**
* Returns the full path to NodeJS global install (if available).
*/
public function getNodeJsGlobalInstallPath()
{
$pathToNodeJS = $this->getGlobalInstallPath("nodejs");
if (!$pathToNodeJS) {
$pathToNodeJS = $this->getGlobalInstallPath("node");
}
return $pathToNodeJS;
}
/**
* Returns the full install path to a command
* @param string $command
*/
public function getGlobalInstallPath($command)
{
if (Environment::isWindows()) {
$result = trim(shell_exec("where /F ".escapeshellarg($command)), "\n\r");
// "Where" can return several lines.
$lines = explode("\n", $result);
return $lines[0];
} else {
// We want to get output from stdout, not from stderr.
// Therefore, we use proc_open.
$descriptorspec = array(
0 => array("pipe", "r"), // stdin
1 => array("pipe", "w"), // stdout
2 => array("pipe", "w"), // stderr
);
$pipes = array();
$process = proc_open("which ".escapeshellarg($command), $descriptorspec, $pipes);
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
// Let's ignore stderr (it is possible we do not find anything and depending on the OS, stderr will
// return things or not)
fclose($pipes[2]);
proc_close($process);
return trim($stdout, "\n\r");
}
}
/**
* Checks if NodeJS is installed locally.
* If yes, will return the version number.
* If no, will return null.
*
* Note: trailing "v" will be removed from version string.
*
* @return null|string
*/
public function getNodeJsLocalInstallVersion($binDir)
{
$returnCode = 0;
$output = "";
$cwd = getcwd();
chdir(__DIR__.'/../../../../');
ob_start();
$version = exec($binDir.DIRECTORY_SEPARATOR.'node -v 2>&1', $output, $returnCode);
ob_end_clean();
chdir($cwd);
if ($returnCode !== 0) {
return;
} else {
return ltrim($version, "v");
}
}
/**
* Returns URL based on version.
* URL is dependent on environment
* @param string $version
* @return string
* @throws NodeJsInstallerException
*/
public function getNodeJSUrl($version)
{
if (Environment::isWindows() && Environment::getArchitecture() == 32) {
if (version_compare($version, '4.0.0') >= 0) {
return "https://nodejs.org/dist/v".$version."/win-x86/node.exe";
} else {
return "https://nodejs.org/dist/v".$version."/node.exe";
}
} elseif (Environment::isWindows() && Environment::getArchitecture() == 64) {
if (version_compare($version, '4.0.0') >= 0) {
return "https://nodejs.org/dist/v" . $version . "/win-x64/node.exe";
} else {
return "https://nodejs.org/dist/v" . $version . "/x64/node.exe";
}
} elseif (Environment::isMacOS() && Environment::getArchitecture() == 32) {
return "https://nodejs.org/dist/v".$version."/node-v".$version."-darwin-x86.tar.gz";
} elseif (Environment::isMacOS() && Environment::getArchitecture() == 64) {
return "https://nodejs.org/dist/v".$version."/node-v".$version."-darwin-x64.tar.gz";
} elseif (Environment::isSunOS() && Environment::getArchitecture() == 32) {
return "https://nodejs.org/dist/v".$version."/node-v".$version."-sunos-x86.tar.gz";
} elseif (Environment::isSunOS() && Environment::getArchitecture() == 64) {
return "https://nodejs.org/dist/v".$version."/node-v".$version."-sunos-x64.tar.gz";
} elseif (Environment::isLinux() && Environment::isArm()) {
if (version_compare($version, '4.0.0') >= 0) {
if (Environment::isArmV6l()) {
return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-armv6l.tar.gz";
} elseif (Environment::isArmV7l()) {
return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-armv7l.tar.gz";
} elseif (Environment::getArchitecture() == 64) {
return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-arm64.tar.gz";
} else {
throw new NodeJsInstallerException('NodeJS-installer cannot install Node on computers with ARM 32bits processors that are not v6l or v7l. Please install NodeJS globally on your machine first, then run composer again.');
}
} else {
throw new NodeJsInstallerException('NodeJS-installer cannot install Node <4.0 on computers with ARM processors. Please install NodeJS globally on your machine first, then run composer again, or consider installing a version of NodeJS >=4.0.');
}
} elseif (Environment::isLinux() && Environment::getArchitecture() == 32) {
return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-x86.tar.gz";
} elseif (Environment::isLinux() && Environment::getArchitecture() == 64) {
return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-x64.tar.gz";
} else {
throw new NodeJsInstallerException('Unsupported architecture: '.PHP_OS.' - '.Environment::getArchitecture().' bits');
}
}
/**
* Installs NodeJS
* @param string $version
* @param string $targetDirectory
* @throws NodeJsInstallerException
*/
public function install($version, $targetDirectory)
{
$this->io->write("Installing NodeJS v".$version."");
$url = $this->getNodeJSUrl($version);
$this->io->write(" Downloading from $url");
$cwd = getcwd();
chdir(__DIR__.'/../../../../');
$fileName = 'vendor/'.pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME);
$this->rfs->copy(parse_url($url, PHP_URL_HOST), $url, $fileName);
if (!file_exists($fileName)) {
throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
.' directory is writable and you have internet connectivity');
}
if (!file_exists($targetDirectory)) {
mkdir($targetDirectory, 0775, true);
}
if (!is_writable($targetDirectory)) {
throw new NodeJsInstallerException("'$targetDirectory' is not writable");
}
if (!Environment::isWindows()) {
// Now, if we are not in Windows, let's untar.
$this->extractTo($fileName, $targetDirectory);
// Let's delete the downloaded file.
unlink($fileName);
} else {
// If we are in Windows, let's move and install NPM.
rename($fileName, $targetDirectory.'/'.basename($fileName));
// We have to download the latest available version in a bin for Windows, then upgrade it:
$url = "https://nodejs.org/dist/npm/npm-1.4.12.zip";
$npmFileName = "vendor/npm-1.4.12.zip";
$this->rfs->copy(parse_url($url, PHP_URL_HOST), $url, $npmFileName);
$this->unzip($npmFileName, $targetDirectory);
unlink($npmFileName);
// Let's update NPM
// 1- Update PATH to run npm.
$path = getenv('PATH');
$newPath = realpath($targetDirectory).";".$path;
putenv('PATH='.$newPath);
// 2- Run npm
$cwd2 = getcwd();
chdir($targetDirectory);
$returnCode = 0;
passthru("npm update npm", $returnCode);
if ($returnCode !== 0) {
throw new NodeJsInstallerException("An error occurred while updating NPM to latest version.");
}
// Finally, let's copy the base npm file for Cygwin
if (file_exists('node_modules/npm/bin/npm')) {
copy('node_modules/npm/bin/npm', 'npm');
}
chdir($cwd2);
}
chdir($cwd);
}
/**
* Extract tar.gz file to target directory.
*
* @param string $tarGzFile
* @param string $targetDir
*/
private function extractTo($tarGzFile, $targetDir)
{
// Note: we cannot use PharData class because it does not keeps symbolic links.
// Also, --strip 1 allows us to remove the first directory.
$output = $return_var = null;
exec("tar -xvf ".$tarGzFile." -C ".escapeshellarg($targetDir)." --strip 1", $output, $return_var);
if ($return_var !== 0) {
throw new NodeJsInstallerException("An error occurred while untaring NodeJS ($tarGzFile) to $targetDir");
}
}
public function createBinScripts($binDir, $targetDir, $isLocal)
{
$cwd = getcwd();
chdir(__DIR__.'/../../../../');
if (!file_exists($binDir)) {
$result = mkdir($binDir, 0775, true);
if ($result === false) {
throw new NodeJsInstallerException("Unable to create directory ".$binDir);
}
}
$fullTargetDir = realpath($targetDir);
$binDir = realpath($binDir);
if (!Environment::isWindows()) {
$this->createBinScript($binDir, $fullTargetDir, 'node', 'node', $isLocal);
$this->createBinScript($binDir, $fullTargetDir, 'npm', 'npm', $isLocal);
} else {
$this->createBinScript($binDir, $fullTargetDir, 'node.bat', 'node', $isLocal);
$this->createBinScript($binDir, $fullTargetDir, 'npm.bat', 'npm', $isLocal);
}
chdir($cwd);
}
/**
* Copy script into $binDir, replacing PATH with $fullTargetDir
* @param string $binDir
* @param string $fullTargetDir
* @param string $scriptName
* @param bool $isLocal
*/
private function createBinScript($binDir, $fullTargetDir, $scriptName, $target, $isLocal)
{
$content = file_get_contents(__DIR__.'/../bin/'.($isLocal ? "local/" : "global/").$scriptName);
if ($isLocal) {
$path = $this->makePathRelative($fullTargetDir, $binDir);
} else {
if ($scriptName == "node") {
$path = $this->getNodeJsGlobalInstallPath();
} else {
$path = $this->getGlobalInstallPath($target);
}
if (strpos($path, $binDir) === 0) {
// we found the local installation that already exists.
return;
}
}
file_put_contents($binDir.'/'.$scriptName, sprintf($content, $path));
chmod($binDir.'/'.$scriptName, 0755);
}
/**
* Shamelessly stolen from Symfony's FileSystem. Thanks guys!
* Given an existing path, convert it to a path relative to a given starting path.
*
* @param string $endPath Absolute path of target
* @param string $startPath Absolute path where traversal begins
*
* @return string Path of target relative to starting path
*/
private function makePathRelative($endPath, $startPath)
{
// Normalize separators on Windows
if ('\\' === DIRECTORY_SEPARATOR) {
$endPath = strtr($endPath, '\\', '/');
$startPath = strtr($startPath, '\\', '/');
}
// Split the paths into arrays
$startPathArr = explode('/', trim($startPath, '/'));
$endPathArr = explode('/', trim($endPath, '/'));
// Find for which directory the common path stops
$index = 0;
while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
$index++;
}
// Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
$depth = count($startPathArr) - $index;
// Repeated "../" for each level need to reach the common path
$traverser = str_repeat('../', $depth);
$endPathRemainder = implode('/', array_slice($endPathArr, $index));
// Construct $endPath from traversing to the common path, then to the remaining $endPath
$relativePath = $traverser.(strlen($endPathRemainder) > 0 ? $endPathRemainder.'/' : '');
return (strlen($relativePath) === 0) ? './' : $relativePath;
}
private function unzip($zipFileName, $targetDir)
{
$zip = new \ZipArchive();
$res = $zip->open($zipFileName);
if ($res === true) {
// extract it to the path we determined above
$zip->extractTo($targetDir);
$zip->close();
} else {
throw new NodeJsInstallerException("Unable to extract file $zipFileName");
}
}
/**
* Adds the vendor/bin directory into the path.
* Note: the vendor/bin is prepended in order to be applied BEFORE an existing install of node.
*
* @param string $binDir
*/
public function registerPath($binDir)
{
$path = getenv('PATH');
if (Environment::isWindows()) {
putenv('PATH='.realpath($binDir).';'.$path);
} else {
putenv('PATH='.realpath($binDir).':'.$path);
}
}
}
src/NodeJsVersionMatcher.php 0000666 00000002732 13436753331 0012120 0 ustar 00 normalize($version);
$versionAsContraint = $versionParser->parseConstraints($normalizedVersion);
$linkConstraint = $versionParser->parseConstraints($constraint);
return $linkConstraint->matches($versionAsContraint);
}
/**
* Finds the best version matching $constraint.
* Will return null if no version matches the constraint.
*
* @param array $versionList
* @param $constraint
* @return string|null
*/
public function findBestMatchingVersion(array $versionList, $constraint)
{
// Let's sort versions in reverse order.
usort($versionList, "version_compare");
$versionList = array_reverse($versionList);
// Now, let's find the best match.
foreach ($versionList as $version) {
if ($this->isVersionMatching($version, $constraint)) {
return $version;
}
}
return;
}
}
src/NodeJsInstallerNodeVersionException.php 0000666 00000000166 13436753331 0015156 0 ustar 00 =5.3.0",
"composer-plugin-api": "^1.0.0",
"ext-openssl": "*"
},
"require-dev": {
"composer/composer": "*"
},
"autoload": {
"psr-4": {
"Mouf\\NodeJsInstaller\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Mouf\\NodeJsInstaller\\": "tests/"
}
},
"extra" : {
"class": ["Mouf\\NodeJsInstaller\\NodeJsPlugin"],
"mouf": {
"logo": "node-js.png"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
phpunit.xml.dist 0000666 00000001052 13436753331 0007731 0 ustar 00
./tests/
node-js.png 0000666 00000006226 13436753331 0006633 0 ustar 00 PNG
IHDR @ @ iq sBIT|d pHYs .# .#x?v 8IDATx[}t՝~$c1u+ 3R)][]%_t+Z mZv[{kI L>&t-]WV`Hf3 $3''w~[;p7pC43`LMނ쭹C$
yi̙d>rtBĨc:|d*|,H0ӻ`Zԫ4$QX_[۴BL(|En(h| +ޕ6u94,3u[ ߳셆XCz $u.TtXΞAz|'ȝQ_o p1I u~LBgq̟?|K^i| x@X@$uHF:vD~Jˇ >볤{gMQP0-Ս;H^r.&@VV'Ǝ*%96A˨p#ܹ MrԹ:=;j~ڽ|}K.C