Skip to content

Instantly share code, notes, and snippets.

@stesie
Last active January 2, 2023 23:42
Show Gist options
  • Save stesie/c9143b98355295420470 to your computer and use it in GitHub Desktop.
Save stesie/c9143b98355295420470 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);
}
}
@stesie
Copy link
Author

stesie commented Jan 5, 2018

@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.

@stesie
Copy link
Author

stesie commented Jan 6, 2018

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