Skip to content

Instantly share code, notes, and snippets.

@zgover
Forked from stesie/V8JsNodeModuleLoader.php
Created August 6, 2022 11:09
Show Gist options
  • Save zgover/8a35076226adcc1a95a21cea1aa06264 to your computer and use it in GitHub Desktop.
Save zgover/8a35076226adcc1a95a21cea1aa06264 to your computer and use it in GitHub Desktop.
<?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);
}
<?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);
}
}
<?php
interface V8JsNodeModuleLoader_FileAccessInterface
{
public function file_get_contents($filePath);
public function file_exists($filePath);
}
<?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);
}
}
<?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;
}
}
<?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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment