Last active
January 2, 2023 23:42
-
-
Save stesie/c9143b98355295420470 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$v8 = new V8Js(); | |
require_once __DIR__.'/V8JsNodeModuleLoader.php'; | |
require_once __DIR__.'/V8JsNodeModuleLoader_NativeFileAccess.php'; | |
$fai = new V8JsNodeModuleLoader_NativeFileAccess(); | |
$loader = new V8JsNodeModuleLoader($fai); | |
$loader->addOverride('fs', 'Hacks/fs'); | |
$loader->addOverride('vm', 'Hacks/vm'); | |
$loader->addOverride('path', 'Hacks/path'); | |
$v8->setModuleNormaliser([ $loader, 'normaliseIdentifier' ]); | |
$v8->setModuleLoader([ $loader, 'loadModule' ]); | |
try { | |
// get handle on coffee-script's compile function | |
$coffeeCompile = $v8->executeString('(require("coffee-script").compile)'); | |
// simple var_dump with comprehension over a range | |
$v8->executeString($coffeeCompile('var_dump(x) for x in [1..5];')); | |
// get handle on a coffee-script function | |
// (we set bare=true here, so it's not safety wrapped; otherwise | |
// we would have to add a return statement) | |
$greeter = $v8->executeString($coffeeCompile( | |
'(name = "Welt") -> var_dump "Hallo #{name}"', | |
[ 'bare' => true ])); | |
// call coffee-script function twice | |
$greeter(); | |
$greeter('Stefan'); | |
} | |
catch (V8JsScriptException $e) { | |
var_dump($e); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require_once __DIR__.'/V8JsNodeModuleLoader_FileAccessInterface.php'; | |
require_once __DIR__.'/V8JsNodeModuleLoader_NormalisePath.php'; | |
/** | |
* Simple Node.js module loader for use with V8Js PHP extension | |
* | |
* This class understands Node.js' node_modules/ directory structure | |
* and can require modules/files from there. | |
* | |
* @copyright 2015,2018 Stefan Siegl <[email protected]> | |
* @author Stefan Siegl <[email protected]> | |
* @package V8JsNodeModuleLoader | |
*/ | |
class V8JsNodeModuleLoader | |
{ | |
use V8JsNodeModuleLoader_NormalisePath; | |
/** | |
* @var V8JsNodeModuleLoader_FileAccessInterface | |
*/ | |
private $fai; | |
/** | |
* Collection of override rules | |
* | |
* "from" identifiers are stored as keys to the array, associated | |
* replacements are the array's values. | |
* | |
* @var string[] | |
*/ | |
private $overrides = array(); | |
/** | |
* Create V8JsNodeModuleLoader instance | |
* | |
* The class needs to query the filesystem (or any replacement) to query | |
* for existing files and loading their data. The interface is a simple | |
* abstraction of that access; if you'd like to just pick any content | |
* from the node_modules/ folder, simply pass an instance of | |
* V8JsNodeModuleLoader_NativeFileAccess. | |
* | |
* @param V8JsNodeModuleLoader_FileAccessInterface $fai | |
* @access public | |
*/ | |
function __construct(V8JsNodeModuleLoader_FileAccessInterface $fai) { | |
$this->fai = $fai; | |
} | |
/** | |
* Normalisation handler, to be passed to V8Js | |
* | |
* @param string $base | |
* @param string $module_name | |
* @access public | |
* @return string[] | |
*/ | |
function normaliseIdentifier($base, $module_name) { | |
if(isset($this->overrides[$module_name])) { | |
$normalisedParts = $this->normalisePath( | |
explode('/', $this->overrides[$module_name])); | |
$moduleName = array_pop($normalisedParts); | |
$normalisedPath = implode('/', $normalisedParts); | |
return array($normalisedPath, $moduleName); | |
} | |
$baseParts = explode('/', $base); | |
$parts = explode('/', $module_name); | |
if($parts[0] == '.' or $parts[0] == '..') { | |
// relative path, prepend base path | |
$parts = array_merge($baseParts, $parts); | |
return $this->handleRelativeLoad($parts); | |
} | |
else { | |
return $this->handleModuleLoad($baseParts, $parts); | |
} | |
} | |
/** | |
* Module loader, to be passed to V8Js | |
* | |
* @param string $moduleName | |
* @access public | |
* @return string|object | |
*/ | |
function loadModule($moduleName) { | |
$filePath = null; | |
foreach (array('', '.js', '.json') as $extension) { | |
if ($this->fai->file_exists($moduleName.$extension)) { | |
$filePath = $moduleName.$extension; | |
break; | |
} | |
} | |
if ($filePath === null) { | |
throw new \Exception('File not found: '.$sourcePath); | |
} | |
$content = $this->fai->file_get_contents($filePath); | |
if (substr($filePath, -5) === '.json') { | |
$content = \json_decode($content); | |
} | |
return $content; | |
} | |
/** | |
* Add a loader override rule | |
* | |
* This can be used to load a V8Js-specific module instead of one | |
* shipped with e.g. a npm package. | |
* | |
* @param mixed $from | |
* @param mixed $to | |
* @access public | |
*/ | |
function addOverride($from, $to) { | |
$this->overrides[$from] = $to; | |
} | |
private function handleRelativeLoad(array $parts) { | |
$normalisedParts = $this->normalisePath($parts); | |
$normalisedId = implode('/', $normalisedParts); | |
if(isset($this->overrides[$normalisedId])) { | |
$normalisedParts = $this->normalisePath( | |
explode('/', $this->overrides[$normalisedId])); | |
$moduleName = array_pop($normalisedParts); | |
$normalisedPath = implode('/', $normalisedParts); | |
return array($normalisedPath, $moduleName); | |
} | |
$sourcePath = implode('/', $normalisedParts); | |
if($this->fai->file_exists($sourcePath) || | |
$this->fai->file_exists($sourcePath.'.js') || | |
$this->fai->file_exists($sourcePath.'.json')) { | |
$moduleName = array_pop($normalisedParts); | |
$normalisedPath = implode('/', $normalisedParts); | |
return array($normalisedPath, $moduleName); | |
} | |
throw new \Exception('File not found: '.$sourcePath); | |
} | |
private function handleModuleLoad(array $baseParts, array $parts) { | |
$moduleName = array_shift($parts); | |
$baseModules = array_keys($baseParts, 'node_modules'); | |
if(empty($baseModules)) { | |
$moduleParts = array(); | |
} | |
else { | |
$moduleParts = array_slice($baseParts, 0, end($baseModules) + 2); | |
} | |
$moduleParts[] = 'node_modules'; | |
$moduleParts[] = $moduleName; | |
$moduleDir = implode('/', $moduleParts); | |
if(!$this->fai->file_exists($moduleDir)) { | |
throw new \Exception('Module not found: ' . $moduleName); | |
} | |
$moduleDir .= '/'; | |
if(empty($parts)) { | |
$packageJsonPath = $moduleDir.'package.json'; | |
if(!$this->fai->file_exists($packageJsonPath)) { | |
throw new \Exception('File not exists: '.$packageJsonPath); | |
} | |
$packageJson = json_decode($this->fai->file_get_contents($packageJsonPath)); | |
if(!isset($packageJson->main)) { | |
throw new \Exception('package.json does not declare main'); | |
} | |
$normalisedParts = $this->normalisePath( | |
array_merge($moduleParts, explode('/', $packageJson->main))); | |
} | |
else { | |
$normalisedParts = $this->normalisePath( | |
array_merge($moduleParts, $parts)); | |
} | |
$moduleName = array_pop($normalisedParts); | |
$normalisedPath = implode('/', $normalisedParts); | |
if(substr($moduleName, -3) == '.js') { | |
$moduleName = substr($moduleName, 0, -3); | |
} | |
return array($normalisedPath, $moduleName); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
interface V8JsNodeModuleLoader_FileAccessInterface | |
{ | |
public function file_get_contents($filePath); | |
public function file_exists($filePath); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require_once __DIR__.'/V8JsNodeModuleLoader_FileAccessInterface.php'; | |
class V8JsNodeModuleLoader_NativeFileAccess | |
implements V8JsNodeModuleLoader_FileAccessInterface | |
{ | |
public function file_get_contents($filePath) | |
{ | |
return file_get_contents($filePath); | |
} | |
public function file_exists($filePath) | |
{ | |
return file_exists($filePath); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
trait V8JsNodeModuleLoader_NormalisePath { | |
function normalisePath(array $parts) { | |
$normalisedParts = array(); | |
array_walk($parts, function($part) use(&$normalisedParts) { | |
switch($part) { | |
case '..': | |
if(!empty($normalisedParts)) { | |
array_pop($normalisedParts); | |
} | |
break; | |
case '.': | |
break; | |
default: | |
array_push($normalisedParts, $part); | |
} | |
}); | |
return $normalisedParts; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require_once __DIR__.'/V8JsNodeModuleLoader.php'; | |
require_once __DIR__.'/V8JsNodeModuleLoader_NormalisePath.php'; | |
class V8JsNodeModuleLoaderTest extends PHPUnit_Framework_TestCase | |
{ | |
use V8JsNodeModuleLoader_NormalisePath; | |
/** | |
* @dataProvider normalisePathProvider | |
*/ | |
public function testNormalisePath($in, $out) | |
{ | |
$mockFai = $this | |
->getMockBuilder('V8JsNodeModuleLoader_FileAccessInterface') | |
->getMock(); | |
$loader = new V8JsNodeModuleLoader($mockFai); | |
$result = $loader->normalisePath($in); | |
$this->assertEquals($out, $result); | |
} | |
public function normalisePathProvider() | |
{ | |
return array( | |
array( | |
array('foo'), | |
array('foo')), | |
array( | |
array('foo', 'bar'), | |
array('foo', 'bar')), | |
array( | |
array('.', 'foo'), | |
array('foo')), | |
array( | |
array('foo', '.', 'bar'), | |
array('foo', 'bar')), | |
array( | |
array('..', 'foo'), | |
array('foo')), | |
array( | |
array('foo', '..', 'bar'), | |
array('bar')), | |
); | |
} | |
public function testNormaliseIdentifierFindsRelativeFiles() | |
{ | |
$mockFai = $this | |
->getMockBuilder('V8JsNodeModuleLoader_FileAccessInterface') | |
->setMethods(array('file_exists', 'file_get_contents')) | |
->getMock(); | |
$mockFai | |
->method('file_exists') | |
->willReturnCallback(function ($path) { return $path === 'node_modules/blar/foo.js'; }); | |
$loader = new V8JsNodeModuleLoader($mockFai); | |
$result = $loader->normaliseIdentifier('node_modules/blar', './foo'); | |
$this->assertEquals(array('node_modules/blar', 'foo'), $result); | |
} | |
/** | |
* @expectedException Exception | |
*/ | |
public function testNormaliseIdentifierThrowsRelativeFileMissing() | |
{ | |
$mockFai = $this | |
->getMockBuilder('V8JsNodeModuleLoader_FileAccessInterface') | |
->setMethods(array('file_exists', 'file_get_contents')) | |
->getMock(); | |
$mockFai | |
->method('file_exists') | |
->withConsecutive( | |
array($this->equalTo('node_modules/blar/foo')), | |
array($this->equalTo('node_modules/blar/foo.js')), | |
array($this->equalTo('node_modules/blar/foo.json')) | |
) | |
->willReturn(false); | |
$loader = new V8JsNodeModuleLoader($mockFai); | |
$result = $loader->normaliseIdentifier('node_modules/blar', './foo'); | |
$this->assertEquals(array('node_modules/blar', 'foo'), $result); | |
} | |
public function testNormaliseIdentifierChecksPackageJson() | |
{ | |
$mockFai = $this | |
->getMockBuilder('V8JsNodeModuleLoader_FileAccessInterface') | |
->setMethods(array('file_exists', 'file_get_contents')) | |
->getMock(); | |
$mockFai | |
->method('file_exists') | |
->withConsecutive( | |
array($this->equalTo('node_modules/blar')), | |
array($this->equalTo('node_modules/blar/package.json')) | |
) | |
->willReturn(true); | |
$mockFai | |
->method('file_get_contents') | |
->with($this->equalTo('node_modules/blar/package.json')) | |
->willReturn(json_encode(array('main' => 'lib/foo'))); | |
$loader = new V8JsNodeModuleLoader($mockFai); | |
$result = $loader->normaliseIdentifier('', 'blar'); | |
$this->assertEquals(array('node_modules/blar/lib', 'foo'), $result); | |
} | |
public function testNormaliseIdentifierLoadsRelativeToModule() | |
{ | |
$mockFai = $this | |
->getMockBuilder('V8JsNodeModuleLoader_FileAccessInterface') | |
->setMethods(array('file_exists', 'file_get_contents')) | |
->getMock(); | |
$mockFai | |
->method('file_exists') | |
->withConsecutive( | |
array($this->equalTo('node_modules/blar')) | |
) | |
->willReturn(true); | |
$loader = new V8JsNodeModuleLoader($mockFai); | |
$result = $loader->normaliseIdentifier('', 'blar/path/to/file'); | |
$this->assertEquals(array('node_modules/blar/path/to', 'file'), $result); | |
} | |
public function testNormaliseIdentifierModuleInModule() | |
{ | |
$mockFai = $this | |
->getMockBuilder('V8JsNodeModuleLoader_FileAccessInterface') | |
->setMethods(array('file_exists', 'file_get_contents')) | |
->getMock(); | |
$mockFai | |
->method('file_exists') | |
->withConsecutive( | |
array($this->equalTo('node_modules/noflo/node_modules/underscore')), | |
array($this->equalTo('node_modules/noflo/node_modules/underscore/package.json')) | |
) | |
->willReturn(true); | |
$mockFai | |
->method('file_get_contents') | |
->with($this->equalTo('node_modules/noflo/node_modules/underscore/package.json')) | |
->willReturn(json_encode(array('main' => 'lib/foo.js'))); | |
$loader = new V8JsNodeModuleLoader($mockFai); | |
$result = $loader->normaliseIdentifier('node_modules/noflo/lib', 'underscore'); | |
$this->assertEquals(array('node_modules/noflo/node_modules/underscore/lib', 'foo'), $result); | |
} | |
public function testNormaliseIdentifierUsesOverrides() | |
{ | |
$mockFai = $this | |
->getMockBuilder('V8JsNodeModuleLoader_FileAccessInterface') | |
->setMethods(array('file_exists', 'file_get_contents')) | |
->getMock(); | |
$mockFai | |
->expects($this->never()) | |
->method('file_exists'); | |
$loader = new V8JsNodeModuleLoader($mockFai); | |
$loader->addOverride('events', 'EventEmitter'); | |
$result = $loader->normaliseIdentifier('', 'events'); | |
$this->assertEquals(array('', 'EventEmitter'), $result); | |
} | |
public function testNormaliseIdentifierUsesOverridesInRelativeLoad() | |
{ | |
$mockFai = $this | |
->getMockBuilder('V8JsNodeModuleLoader_FileAccessInterface') | |
->setMethods(array('file_exists', 'file_get_contents')) | |
->getMock(); | |
$mockFai | |
->expects($this->never()) | |
->method('file_exists'); | |
$loader = new V8JsNodeModuleLoader($mockFai); | |
$loader->addOverride('node_modules/noflo/lib/Platform', 'Hacks/Platform'); | |
$result = $loader->normaliseIdentifier('node_modules/noflo/lib', './Platform'); | |
$this->assertEquals(array('Hacks', 'Platform'), $result); | |
} | |
} |
Gist updated to allow require of JSON files (like package.json
), depends on native module pull request.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@mortenson sorry for not noticing your comment earlier (seems like I either didn't get notified or just missed it)
The Hacks/ files actually were just empty, didn't need concrete implementations so far.