├── .github └── workflows │ └── main.yml ├── .gitignore ├── .jshintignore ├── .jshintrc ├── MIT-LICENSE.txt ├── README.md ├── api.js ├── api ├── async.js ├── psync.js └── sync.js ├── index.js ├── package-lock.json ├── package.json ├── src ├── API.js ├── Environment │ ├── Environment.js │ ├── EnvironmentFactory.js │ └── EnvironmentProvider.js ├── FileSystem.js ├── IO.js ├── Initialiser │ ├── InitialiserContext.js │ └── InitialiserLoader.js ├── Loader.js ├── ModuleRepository.js ├── Performance.js ├── Transformer.js ├── TransformerFactory.js ├── php │ └── initialiser_stub.php └── shared │ └── initialiserLoader.js └── test ├── .jshintrc ├── integration ├── api │ └── environment │ │ ├── directRequireTest.js │ │ └── moduleRequireTest.js ├── apiTest.js ├── fixtures │ ├── asyncBootstraps │ │ ├── bootstrap │ │ │ ├── first.php │ │ │ └── second.php │ │ ├── my_include.php │ │ └── uniter.config.js │ ├── asyncModeViaModeOption │ │ └── uniter.config.js │ ├── asyncModeViaSyncOption │ │ └── uniter.config.js │ ├── customRule │ │ ├── config.phptoast.js │ │ ├── config.transpiler.js │ │ ├── plugin.js │ │ └── uniter.config.js │ ├── moduleRequire │ │ ├── module_1.php │ │ ├── module_2.php │ │ └── uniter.config.js │ ├── psyncMode │ │ └── uniter.config.js │ ├── stubFilesOption │ │ └── uniter.config.js │ ├── stubOption │ │ ├── my │ │ │ └── stuff │ │ │ │ ├── first_polyfill.php │ │ │ │ ├── second_polyfill.php │ │ │ │ └── third_polyfill.php │ │ └── uniter.config.js │ └── syncMode │ │ └── uniter.config.js ├── tools.js ├── transpile │ ├── options │ │ ├── bootstrapsOptionTest.js │ │ ├── stubFilesOptionTest.js │ │ └── stubOptionTest.js │ └── pathsTest.js └── transpileTest.js └── unit ├── APITest.js ├── Environment ├── EnvironmentFactoryTest.js ├── EnvironmentProviderTest.js └── EnvironmentTest.js ├── FileSystemTest.js ├── IOTest.js ├── Initialiser ├── InitialiserContextTest.js └── InitialiserLoaderTest.js ├── LoaderTest.js ├── ModuleRepositoryTest.js ├── PerformanceTest.js └── TransformerTest.js /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x, 22.x] 12 | 13 | steps: 14 | # Check out the repository under $GITHUB_WORKSPACE, so this job can access it. 15 | - uses: actions/checkout@v2 16 | 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Install NPM dependencies 23 | run: npm ci 24 | 25 | - name: Run tests 26 | run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ._.DS_Store 3 | /.idea 4 | /node_modules 5 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "forin": true, 6 | "immed": true, 7 | "indent": 4, 8 | "latedef": true, 9 | "maxlen": false, 10 | "maxparams": false, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": true, 15 | "plusplus": false, 16 | "quotmark": "single", 17 | "trailing": true, 18 | "undef": true, 19 | "unused": true, 20 | "strict": true, 21 | "node": true 22 | } 23 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 Dan Phillimore (asmblah) 2 | http://github.com/uniter/phpify/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHPify 2 | ====== 3 | 4 | [![Build Status](https://github.com/uniter/phpify/workflows/CI/badge.svg)](https://github.com/uniter/phpify/actions?query=workflow%3ACI) 5 | 6 | Compiles PHP modules for the browser with [Uniter][]. 7 | 8 | For the Webpack loader, see [Uniter-Loader][]. 9 | 10 | [Uniter]: https://github.com/asmblah/uniter 11 | [Uniter-Loader]: https://github.com/uniter/loader 12 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | // Shorthand for referencing the asynchronous runtime API 13 | module.exports = require('./api/async'); 14 | -------------------------------------------------------------------------------- /api/async.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /*global global */ 13 | var API = require('../src/API'), 14 | Environment = require('../src/Environment/Environment'), 15 | EnvironmentFactory = require('../src/Environment/EnvironmentFactory'), 16 | EnvironmentProvider = require('../src/Environment/EnvironmentProvider'), 17 | FileSystem = require('../src/FileSystem'), 18 | InitialiserContext = require('../src/Initialiser/InitialiserContext'), 19 | IO = require('../src/IO'), 20 | Loader = require('../src/Loader'), 21 | ModuleRepository = require('../src/ModuleRepository'), 22 | Performance = require('../src/Performance'), 23 | performance = new Performance(Date, global), 24 | phpConfigImporter = require('phpconfig').configImporter, 25 | phpRuntime = require('phpruntime'), 26 | io = new IO(console), 27 | environmentFactory = new EnvironmentFactory(Environment), 28 | environmentProvider = new EnvironmentProvider(environmentFactory, phpRuntime, performance, io), 29 | initialiserLoader = require('../src/shared/initialiserLoader'), 30 | api = new API( 31 | FileSystem, 32 | Loader, 33 | ModuleRepository, 34 | InitialiserContext, 35 | environmentProvider, 36 | initialiserLoader, 37 | phpConfigImporter, 38 | require.cache 39 | ), 40 | loader = api.createLoader(); 41 | 42 | module.exports = loader; 43 | -------------------------------------------------------------------------------- /api/psync.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /*global global */ 13 | var API = require('../src/API'), 14 | Environment = require('../src/Environment/Environment'), 15 | EnvironmentFactory = require('../src/Environment/EnvironmentFactory'), 16 | EnvironmentProvider = require('../src/Environment/EnvironmentProvider'), 17 | FileSystem = require('../src/FileSystem'), 18 | InitialiserContext = require('../src/Initialiser/InitialiserContext'), 19 | IO = require('../src/IO'), 20 | Loader = require('../src/Loader'), 21 | ModuleRepository = require('../src/ModuleRepository'), 22 | Performance = require('../src/Performance'), 23 | performance = new Performance(Date, global), 24 | phpConfigImporter = require('phpconfig').configImporter, 25 | phpRuntime = require('phpruntime/psync'), 26 | io = new IO(console), 27 | environmentFactory = new EnvironmentFactory(Environment), 28 | environmentProvider = new EnvironmentProvider(environmentFactory, phpRuntime, performance, io), 29 | initialiserLoader = require('../src/shared/initialiserLoader'), 30 | api = new API( 31 | FileSystem, 32 | Loader, 33 | ModuleRepository, 34 | InitialiserContext, 35 | environmentProvider, 36 | initialiserLoader, 37 | phpConfigImporter, 38 | require.cache 39 | ), 40 | loader = api.createLoader(); 41 | 42 | module.exports = loader; 43 | -------------------------------------------------------------------------------- /api/sync.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /*global global */ 13 | var API = require('../src/API'), 14 | Environment = require('../src/Environment/Environment'), 15 | EnvironmentFactory = require('../src/Environment/EnvironmentFactory'), 16 | EnvironmentProvider = require('../src/Environment/EnvironmentProvider'), 17 | FileSystem = require('../src/FileSystem'), 18 | InitialiserContext = require('../src/Initialiser/InitialiserContext'), 19 | IO = require('../src/IO'), 20 | Loader = require('../src/Loader'), 21 | ModuleRepository = require('../src/ModuleRepository'), 22 | Performance = require('../src/Performance'), 23 | performance = new Performance(Date, global), 24 | phpConfigImporter = require('phpconfig').configImporter, 25 | phpRuntime = require('phpruntime/sync'), 26 | io = new IO(console), 27 | environmentFactory = new EnvironmentFactory(Environment), 28 | environmentProvider = new EnvironmentProvider(environmentFactory, phpRuntime, performance, io), 29 | initialiserLoader = require('../src/shared/initialiserLoader'), 30 | api = new API( 31 | FileSystem, 32 | Loader, 33 | ModuleRepository, 34 | InitialiserContext, 35 | environmentProvider, 36 | initialiserLoader, 37 | phpConfigImporter, 38 | require.cache 39 | ), 40 | loader = api.createLoader(); 41 | 42 | module.exports = loader; 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /* 13 | * Transform API: used by the Webpack loader, Babel plugin etc. 14 | */ 15 | 16 | var Transformer = require('./src/Transformer'), 17 | TransformerFactory = require('./src/TransformerFactory'), 18 | fs = require('fs'), 19 | globby = require('globby'), 20 | path = require('path'), 21 | phpConfigLoader = require('phpconfig').createConfigLoader(fs.existsSync), 22 | phpToAST = require('phptoast'), 23 | phpToJS = require('phptojs'), 24 | initialiserStubPath = path.resolve(__dirname + '/src/php/initialiser_stub.php'), 25 | transformerFactory = new TransformerFactory( 26 | Transformer, 27 | phpConfigLoader, 28 | phpToAST, 29 | phpToJS, 30 | require.resolve, 31 | globby, 32 | initialiserStubPath 33 | ); 34 | 35 | module.exports = transformerFactory; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6.1.0", 3 | "name": "phpify", 4 | "description": "Compiles PHP modules for the browser with Uniter", 5 | "keywords": [ 6 | "php", 7 | "browser", 8 | "client-side", 9 | "uniter", 10 | "transform" 11 | ], 12 | "homepage": "https://github.com/uniter/phpify", 13 | "author": "Dan Phillimore (https://github.com/asmblah)", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/uniter/phpify" 17 | }, 18 | "bugs": { 19 | "email": "dan@ovms.co", 20 | "url": "https://github.com/uniter/phpify/issues" 21 | }, 22 | "main": "index", 23 | "scripts": { 24 | "jshint": "jshint .", 25 | "mocha": "mocha -r mocha-bootstrap --recursive test/", 26 | "test": "npm run jshint && npm run mocha" 27 | }, 28 | "dependencies": { 29 | "globby": "^11.0.2", 30 | "lie": "^3.3.0", 31 | "microdash": "^1.4.2", 32 | "nowdoc": "~1.0", 33 | "phpconfig": "^1.0.4", 34 | "phpruntime": "^9.2.1", 35 | "phptoast": "^9.3.0", 36 | "phptojs": "^10.2.0", 37 | "require-resolve": "0.0.2" 38 | }, 39 | "devDependencies": { 40 | "chai": "^4.3.10", 41 | "chai-as-promised": "^7.1.1", 42 | "jshint": "^2.13.6", 43 | "mocha": "^11.1.0", 44 | "mocha-bootstrap": "^1.1.1", 45 | "sinon": "^17.0.1", 46 | "sinon-chai": "^3.7.0" 47 | }, 48 | "engines": { 49 | "node": ">=0.6" 50 | }, 51 | "license": "MIT" 52 | } 53 | -------------------------------------------------------------------------------- /src/API.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'); 13 | 14 | /** 15 | * API entry point for creating Loaders for compiled PHP modules to use 16 | * 17 | * @param {class} FileSystem 18 | * @param {class} Loader 19 | * @param {class} ModuleRepository 20 | * @param {class} InitialiserContext 21 | * @param {EnvironmentProvider} environmentProvider 22 | * @param {InitialiserLoader} initialiserLoader 23 | * @param {ConfigImporter} phpConfigImporter 24 | * @param {Object} requireCache 25 | * @constructor 26 | */ 27 | function API( 28 | FileSystem, 29 | Loader, 30 | ModuleRepository, 31 | InitialiserContext, 32 | environmentProvider, 33 | initialiserLoader, 34 | phpConfigImporter, 35 | requireCache 36 | ) { 37 | /** 38 | * @type {EnvironmentProvider} 39 | */ 40 | this.environmentProvider = environmentProvider; 41 | /** 42 | * @type {class} 43 | */ 44 | this.FileSystem = FileSystem; 45 | /** 46 | * @type {class} 47 | */ 48 | this.InitialiserContext = InitialiserContext; 49 | /** 50 | * @type {InitialiserLoader} 51 | */ 52 | this.initialiserLoader = initialiserLoader; 53 | /** 54 | * @type {class} 55 | */ 56 | this.Loader = Loader; 57 | /** 58 | * @type {class} 59 | */ 60 | this.ModuleRepository = ModuleRepository; 61 | /** 62 | * @type {ConfigImporter} 63 | */ 64 | this.phpConfigImporter = phpConfigImporter; 65 | /** 66 | * @type {Object} 67 | */ 68 | this.requireCache = requireCache; 69 | } 70 | 71 | _.extend(API.prototype, { 72 | /** 73 | * Creates a new, isolated Loader along with a FileSystem 74 | * and PHPCore/PHPRuntime environment for compiled PHP modules to use. 75 | * 76 | * @returns {Loader} 77 | */ 78 | createLoader: function () { 79 | var api = this, 80 | initialiserContext = new api.InitialiserContext(), 81 | moduleRepository = new api.ModuleRepository(api.requireCache), 82 | fileSystem = new api.FileSystem(moduleRepository), 83 | loader = new api.Loader( 84 | moduleRepository, 85 | initialiserContext, 86 | fileSystem, 87 | api.environmentProvider, 88 | api.phpConfigImporter 89 | ), 90 | initialiser = api.initialiserLoader.loadInitialiser(); 91 | 92 | initialiser(loader); 93 | 94 | return loader; 95 | } 96 | }); 97 | 98 | module.exports = API; 99 | -------------------------------------------------------------------------------- /src/Environment/Environment.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'), 13 | Promise = require('lie'); 14 | 15 | /** 16 | * Represents the PHP environment for a loader. 17 | * 18 | * @param {ModuleRepository} moduleRepository 19 | * @param {InitialiserContext} initialiserContext 20 | * @param {Object} phpCoreEnvironment 21 | * @constructor 22 | */ 23 | function Environment(moduleRepository, initialiserContext, phpCoreEnvironment) { 24 | /** 25 | * @type {boolean} 26 | */ 27 | this.bootstrapped = false; 28 | /** 29 | * @type {InitialiserContext} 30 | */ 31 | this.initialiserContext = initialiserContext; 32 | /** 33 | * @type {ModuleRepository} 34 | */ 35 | this.moduleRepository = moduleRepository; 36 | /** 37 | * @type {Object} 38 | */ 39 | this.phpCoreEnvironment = phpCoreEnvironment; 40 | } 41 | 42 | _.extend(Environment.prototype, { 43 | /** 44 | * Returns the underlying PHPCore environment. 45 | * 46 | * @returns {Object} 47 | */ 48 | getPhpCoreEnvironment: function () { 49 | return this.phpCoreEnvironment; 50 | }, 51 | 52 | /** 53 | * Requires a PHP module from JS-land. 54 | * 55 | * @param {string} filePath 56 | * @returns {Promise}|Value 57 | */ 58 | requireModule: function (filePath) { 59 | var bootstraps, 60 | environment = this, 61 | mode, 62 | moduleFactory = environment.moduleRepository.getModuleFactory(filePath), 63 | phpCoreEnvironment = this.phpCoreEnvironment; 64 | 65 | if (environment.bootstrapped) { 66 | return moduleFactory({}, phpCoreEnvironment).execute(); 67 | } 68 | 69 | environment.bootstrapped = true; 70 | 71 | bootstraps = environment.initialiserContext.getBootstraps(); 72 | mode = phpCoreEnvironment.getMode(); 73 | 74 | if (mode === 'sync') { 75 | bootstraps.forEach(function (bootstrap) { 76 | bootstrap(phpCoreEnvironment); 77 | }); 78 | 79 | return moduleFactory({}, phpCoreEnvironment).execute(); 80 | } 81 | 82 | return new Promise(function (resolve, reject) { 83 | var pendingBootstraps = bootstraps.slice(); 84 | 85 | function dequeueBootstrap() { 86 | var bootstrap, 87 | result; 88 | 89 | if (pendingBootstraps.length === 0) { 90 | resolve(moduleFactory({}, phpCoreEnvironment).execute()); 91 | 92 | return; 93 | } 94 | 95 | bootstrap = pendingBootstraps.shift(); 96 | 97 | result = bootstrap(phpCoreEnvironment); 98 | 99 | if (result) { 100 | result.then(dequeueBootstrap, reject); 101 | } else { 102 | dequeueBootstrap(); 103 | } 104 | } 105 | 106 | dequeueBootstrap(); 107 | }); 108 | } 109 | }); 110 | 111 | module.exports = Environment; 112 | -------------------------------------------------------------------------------- /src/Environment/EnvironmentFactory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'); 13 | 14 | /** 15 | * Creates the Environment for a loader. 16 | * 17 | * @param {class} Environment 18 | * @constructor 19 | */ 20 | function EnvironmentFactory(Environment) { 21 | /** 22 | * @type {class} 23 | */ 24 | this.Environment = Environment; 25 | } 26 | 27 | _.extend(EnvironmentFactory.prototype, { 28 | /** 29 | * Creates a new Environment. 30 | * 31 | * @param {ModuleRepository} moduleRepository 32 | * @param {InitialiserContext} initialiserContext 33 | * @param {Object} phpCoreEnvironment 34 | * @returns {Environment} 35 | */ 36 | createEnvironment: function ( 37 | moduleRepository, 38 | initialiserContext, 39 | phpCoreEnvironment 40 | ) { 41 | return new this.Environment(moduleRepository, initialiserContext, phpCoreEnvironment); 42 | } 43 | }); 44 | 45 | module.exports = EnvironmentFactory; 46 | -------------------------------------------------------------------------------- /src/Environment/EnvironmentProvider.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'); 13 | 14 | /** 15 | * Creates the Environment for a loader. 16 | * 17 | * @param {EnvironmentFactory} environmentFactory 18 | * @param {Object} phpRuntime 19 | * @param {Performance} performance 20 | * @param {IO} io 21 | * @constructor 22 | */ 23 | function EnvironmentProvider(environmentFactory, phpRuntime, performance, io) { 24 | /** 25 | * @type {EnvironmentFactory} 26 | */ 27 | this.environmentFactory = environmentFactory; 28 | /** 29 | * @type {IO} 30 | */ 31 | this.io = io; 32 | /** 33 | * @type {Performance} 34 | */ 35 | this.performance = performance; 36 | /** 37 | * @type {Object} 38 | */ 39 | this.phpRuntime = phpRuntime; 40 | } 41 | 42 | _.extend(EnvironmentProvider.prototype, { 43 | /** 44 | * Creates a new Environment. 45 | * 46 | * @param {ModuleRepository} moduleRepository 47 | * @param {InitialiserContext} initialiserContext 48 | * @param {FileSystem} fileSystem 49 | * @param {Object} phpifyConfig 50 | * @param {Object} phpCoreConfig 51 | * @returns {Environment} 52 | */ 53 | createEnvironment: function ( 54 | moduleRepository, 55 | initialiserContext, 56 | fileSystem, 57 | phpifyConfig, 58 | phpCoreConfig 59 | ) { 60 | var provider = this, 61 | environmentOptions = Object.assign({}, phpCoreConfig, { 62 | fileSystem: fileSystem, 63 | include: function (filePath, promise) { 64 | var result; 65 | 66 | try { 67 | result = fileSystem.getModuleFactory(filePath); 68 | } catch (error) { 69 | promise.reject(error); 70 | return; 71 | } 72 | 73 | promise.resolve(result); 74 | }, 75 | performance: provider.performance 76 | }), 77 | addons = environmentOptions.addons || [], 78 | phpCoreEnvironment; 79 | 80 | delete environmentOptions.addons; 81 | 82 | phpCoreEnvironment = provider.phpRuntime.createEnvironment( 83 | environmentOptions, 84 | addons 85 | ); 86 | 87 | provider.io.install(phpCoreEnvironment, phpifyConfig); 88 | 89 | return this.environmentFactory.createEnvironment(moduleRepository, initialiserContext, phpCoreEnvironment); 90 | } 91 | }); 92 | 93 | module.exports = EnvironmentProvider; 94 | -------------------------------------------------------------------------------- /src/FileSystem.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'), 13 | hasOwn = {}.hasOwnProperty, 14 | path = require('path'), 15 | Promise = require('lie'); 16 | 17 | /** 18 | * Virtual FileSystem for use in the browser with compiled PHP modules 19 | * 20 | * @param {ModuleRepository} moduleRepository 21 | * @constructor 22 | */ 23 | function FileSystem(moduleRepository) { 24 | /** 25 | * @type {Object.} 26 | */ 27 | this.files = {}; 28 | /** 29 | * @type {ModuleRepository} 30 | */ 31 | this.moduleRepository = moduleRepository; 32 | } 33 | 34 | _.extend(FileSystem.prototype, { 35 | /** 36 | * Fetches the module wrapper factory function for a compiled PHP module, 37 | * if it exists in the compiled bundle 38 | * 39 | * @param {string} filePath 40 | * @returns {Function} 41 | * @throws {Error} Throws when the specified compiled module does not exist 42 | */ 43 | getModuleFactory: function (filePath) { 44 | var fileSystem = this; 45 | 46 | filePath = fileSystem.realPath(filePath); 47 | 48 | // TODO: If a PHP source file has been written to the virtual FS, and eval-ish support 49 | // is installed, allow the dynamically-generated module to be compiled and run 50 | 51 | return fileSystem.moduleRepository.getModuleFactory(filePath); 52 | }, 53 | 54 | /** 55 | * Determines whether the specified directory path exists in the FileSystem. 56 | * Currently always returns true, as we cannot be sure from the info we have 57 | * 58 | * @returns {boolean} 59 | */ 60 | isDirectory: function () { 61 | // TODO: Implement once we have support for non-PHP files in the VFS 62 | return false; 63 | }, 64 | 65 | /** 66 | * Determines whether the specified file exists in the FileSystem. 67 | * Currently only compiled PHP modules can be in the FileSystem, so only those 68 | * may be detected. 69 | * 70 | * @param {string} filePath 71 | * @returns {boolean} 72 | */ 73 | isFile: function (filePath) { 74 | var fileSystem = this; 75 | 76 | filePath = fileSystem.realPath(filePath); 77 | 78 | return hasOwn.call(fileSystem.files, filePath) || 79 | fileSystem.moduleRepository.moduleExists(filePath); 80 | }, 81 | 82 | /** 83 | * Opens a Stream for the specified file asynchronously 84 | * 85 | * @param {string} filePath 86 | * @returns {Promise} Resolves with a Stream for the file on success, rejects on failure 87 | */ 88 | open: function (filePath) { 89 | return new Promise(function (resolve, reject) { 90 | reject(new Error('Could not open "' + filePath + '" :: Streams are not currently supported by PHPify')); 91 | }); 92 | }, 93 | 94 | /** 95 | * Opens a Stream for the specified file synchronously 96 | * 97 | * @param {string} filePath 98 | * @returns {Stream} 99 | */ 100 | openSync: function (filePath) { 101 | throw new Error('Could not open "' + filePath + '" :: Streams are not currently supported by PHPify'); 102 | }, 103 | 104 | /** 105 | * Converts the specified module path to a full one, 106 | * normalizing any parent- or current-directory symbols 107 | * 108 | * @param {string} filePath 109 | * @returns {string} 110 | */ 111 | realPath: function (filePath) { 112 | filePath = path.normalize(filePath); 113 | 114 | // Strip any leading slash, as the virtual FS does not expect it 115 | filePath = filePath.replace(/^\/+/, ''); 116 | 117 | return filePath; 118 | }, 119 | 120 | /** 121 | * Deletes a file or folder asynchronously 122 | * 123 | * @param {string} filePath 124 | * @returns {Promise} Resolves on success, rejects on failure 125 | */ 126 | unlink: function (filePath) { 127 | return new Promise(function (resolve, reject) { 128 | reject(new Error('Could not delete "' + filePath + '" :: not currently supported by PHPify')); 129 | }); 130 | }, 131 | 132 | /** 133 | * Deletes a file or folder synchronously 134 | * 135 | * @param {string} filePath 136 | */ 137 | unlinkSync: function (filePath) { 138 | throw new Error('Could not delete "' + filePath + '" :: not currently supported by PHPify'); 139 | }, 140 | 141 | /** 142 | * Writes the contents of a file to the virtual FileSystem. 143 | * 144 | * @param {string} path 145 | * @param {string} contents 146 | */ 147 | writeFile: function (path, contents) { 148 | var fileSystem = this; 149 | 150 | fileSystem.files[path] = contents; 151 | } 152 | }); 153 | 154 | module.exports = FileSystem; 155 | -------------------------------------------------------------------------------- /src/IO.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'); 13 | 14 | /** 15 | * Hooks Uniter's PHP stdout and stderr streams up to the console, if available and enabled 16 | * 17 | * @param {Console} console 18 | * @constructor 19 | */ 20 | function IO(console) { 21 | /** 22 | * @type {Console} 23 | */ 24 | this.console = console; 25 | } 26 | 27 | _.extend(IO.prototype, { 28 | /** 29 | * Hooks the IO for a PHP engine up to the console 30 | * 31 | * @param {Environment} environment 32 | * @param {Object} phpifyConfig 33 | */ 34 | install: function (environment, phpifyConfig) { 35 | var io = this; 36 | 37 | if (!io.console) { 38 | // Console is not available - nothing to do 39 | return; 40 | } 41 | 42 | if (phpifyConfig.stdio === false) { 43 | // Standard I/O has been disabled in config - nothing to do 44 | return; 45 | } 46 | 47 | environment.getStdout().on('data', function (data) { 48 | io.console.info(data); 49 | }); 50 | 51 | environment.getStderr().on('data', function (data) { 52 | io.console.warn(data); 53 | }); 54 | } 55 | }); 56 | 57 | module.exports = IO; 58 | -------------------------------------------------------------------------------- /src/Initialiser/InitialiserContext.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'); 13 | 14 | /** 15 | * Contains the initialiser context data. 16 | * 17 | * @constructor 18 | */ 19 | function InitialiserContext() { 20 | /** 21 | * @type {Function|null} 22 | */ 23 | this.bootstrapFetcher = null; 24 | /** 25 | * @type {boolean} 26 | */ 27 | this.loadingBootstraps = false; 28 | } 29 | 30 | _.extend(InitialiserContext.prototype, { 31 | /** 32 | * Adds zero or more bootstraps to be executed within the environment. 33 | * Must be done as a separate method call from .installModules(...), as the PHP module factory fetcher 34 | * function installed needs to be available here, because bootstrap modules may themselves 35 | * be PHP modules (useful for including Composer's autoloader, for example). 36 | * 37 | * @param {Function} bootstrapFetcher 38 | */ 39 | bootstrap: function (bootstrapFetcher) { 40 | this.bootstrapFetcher = bootstrapFetcher; 41 | }, 42 | 43 | /** 44 | * Fetches PHP bootstraps registered by the initialiser. 45 | * 46 | * @returns {Function[]} 47 | */ 48 | getBootstraps: function () { 49 | var bootstraps, 50 | context = this; 51 | 52 | if (context.bootstrapFetcher) { 53 | context.loadingBootstraps = true; 54 | bootstraps = context.bootstrapFetcher(); 55 | context.loadingBootstraps = false; 56 | } else { 57 | bootstraps = []; 58 | } 59 | 60 | return bootstraps; 61 | }, 62 | 63 | /** 64 | * Determines whether the initialiser context is currently loading PHP bootstraps. 65 | * 66 | * @returns {boolean} 67 | */ 68 | isLoadingBootstraps: function () { 69 | return this.loadingBootstraps; 70 | } 71 | }); 72 | 73 | module.exports = InitialiserContext; 74 | -------------------------------------------------------------------------------- /src/Initialiser/InitialiserLoader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'); 13 | 14 | /** 15 | * Loads the PHP initialiser. 16 | * 17 | * @param {Function} initialiserRequirer 18 | * @constructor 19 | */ 20 | function InitialiserLoader(initialiserRequirer) { 21 | /** 22 | * @type {Function} 23 | */ 24 | this.initialiserRequirer = initialiserRequirer; 25 | } 26 | 27 | _.extend(InitialiserLoader.prototype, { 28 | /** 29 | * Loads the initialiser via the registered requirer. 30 | * 31 | * @returns {Function} 32 | */ 33 | loadInitialiser: function () { 34 | return this.initialiserRequirer(); 35 | }, 36 | 37 | /** 38 | * Installs a new initialiser requirer. 39 | * 40 | * @param {Function} initialiserRequirer 41 | */ 42 | setRequirer: function (initialiserRequirer) { 43 | this.initialiserRequirer = initialiserRequirer; 44 | } 45 | }); 46 | 47 | module.exports = InitialiserLoader; 48 | -------------------------------------------------------------------------------- /src/Loader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'); 13 | 14 | /** 15 | * Public API for compiled PHP modules. 16 | * 17 | * @param {ModuleRepository} moduleRepository 18 | * @param {InitialiserContext} initialiserContext 19 | * @param {FileSystem} fileSystem 20 | * @param {EnvironmentProvider} environmentProvider 21 | * @param {ConfigImporterInterface} phpConfigImporter 22 | * @constructor 23 | */ 24 | function Loader( 25 | moduleRepository, 26 | initialiserContext, 27 | fileSystem, 28 | environmentProvider, 29 | phpConfigImporter 30 | ) { 31 | /** 32 | * @type {Environment|null} Lazily-initialised by .getEnvironment() 33 | */ 34 | this.environment = null; 35 | /** 36 | * @type {EnvironmentProvider} 37 | */ 38 | this.environmentProvider = environmentProvider; 39 | /** 40 | * @type {FileSystem} 41 | */ 42 | this.fileSystem = fileSystem; 43 | /** 44 | * @type {InitialiserContext} 45 | */ 46 | this.initialiserContext = initialiserContext; 47 | /** 48 | * @type {ModuleRepository} 49 | */ 50 | this.moduleRepository = moduleRepository; 51 | /** 52 | * @type {ConfigImporterInterface} 53 | */ 54 | this.phpConfigImporter = phpConfigImporter; 55 | /** 56 | * @type {Object} Populated from the Initialiser by .configure(...). 57 | */ 58 | this.phpCoreConfig = {}; 59 | /** 60 | * @type {Object} Populated from the Initialiser by .configure(...). 61 | */ 62 | this.phpifyConfig = {}; 63 | } 64 | 65 | _.extend(Loader.prototype, { 66 | /** 67 | * Adds zero or more bootstraps to be executed within the environment. 68 | * Must be done as a separate method call from .installModules(...), as the PHP module factory fetcher 69 | * function installed needs to be available here, because bootstrap modules may themselves 70 | * be PHP modules (useful for including Composer's autoloader, for example). 71 | * 72 | * @param {Function} bootstrapFetcher 73 | * @returns {Loader} For chaining 74 | */ 75 | bootstrap: function (bootstrapFetcher) { 76 | var loader = this; 77 | 78 | loader.initialiserContext.bootstrap(bootstrapFetcher); 79 | 80 | return loader; 81 | }, 82 | 83 | /** 84 | * Populates the PHPify and PHPCore configurations 85 | * 86 | * @param {Object} phpifyConfig 87 | * @param {Object[]} phpCoreConfigs 88 | * @returns {Loader} For chaining 89 | */ 90 | configure: function (phpifyConfig, phpCoreConfigs) { 91 | var loader = this; 92 | 93 | loader.phpifyConfig = phpifyConfig; 94 | loader.phpCoreConfig = loader.phpConfigImporter 95 | .importLibrary({configs: phpCoreConfigs}) 96 | .mergeAll(); 97 | 98 | return loader; 99 | }, 100 | 101 | /** 102 | * Creates a new Environment. 103 | * 104 | * @param {Object=} phpCoreConfig 105 | * @param {Object=} phpifyConfig 106 | * @returns {Environment} 107 | */ 108 | createEnvironment: function (phpCoreConfig, phpifyConfig) { 109 | var addons, 110 | loader = this; 111 | 112 | // TODO: Do something better for config merge as in PHPConfig. 113 | addons = (loader.phpCoreConfig.addons || []).concat((phpCoreConfig || {}).addons || []); 114 | phpCoreConfig = Object.assign({}, loader.phpCoreConfig, phpCoreConfig || {}); 115 | phpCoreConfig.addons = addons; 116 | 117 | phpifyConfig = Object.assign({}, loader.phpifyConfig, phpifyConfig || {}); 118 | 119 | return loader.environmentProvider.createEnvironment( 120 | loader.moduleRepository, 121 | loader.initialiserContext, 122 | loader.fileSystem, 123 | phpifyConfig, 124 | phpCoreConfig 125 | ); 126 | }, 127 | 128 | /** 129 | * Fetches the Environment for this loader, creating it if necessary 130 | * 131 | * @return {Environment} 132 | */ 133 | getEnvironment: function () { 134 | var loader = this; 135 | 136 | if (!loader.environment) { 137 | loader.environment = loader.createEnvironment(); 138 | } 139 | 140 | return loader.environment; 141 | }, 142 | 143 | /** 144 | * Fetches the module wrapper factory function for a compiled PHP module, 145 | * if it exists in the compiled bundle 146 | * 147 | * @param {string} filePath 148 | * @returns {Function} 149 | * @throws {Error} Throws when the specified compiled module does not exist 150 | */ 151 | getModuleFactory: function (filePath) { 152 | return this.moduleRepository.getModuleFactory(filePath); 153 | }, 154 | 155 | /** 156 | * Installs a function into the loader for fetching the compiled module wrappers of PHP modules 157 | * 158 | * @param {Function} phpModuleFactoryFetcher 159 | * @returns {Loader} For chaining 160 | */ 161 | installModules: function (phpModuleFactoryFetcher) { 162 | var loader = this; 163 | 164 | loader.moduleRepository.init(phpModuleFactoryFetcher); 165 | 166 | return loader; 167 | }, 168 | 169 | /** 170 | * Determines whether this loader has already been initialised 171 | * (whether the Environment has been created, lazily, when loading a PHP module) 172 | * 173 | * @return {boolean} 174 | */ 175 | isInitialised: function () { 176 | return this.environment !== null; 177 | }, 178 | 179 | /** 180 | * Configures the environment and path for the given module, and either executes it 181 | * and exports the result or just exports the module factory depending on mode. 182 | * Used by all compiled PHP modules. 183 | * 184 | * @param {string} filePath 185 | * @param {Object} module CommonJS module object 186 | * @param {Function} moduleFactory 187 | */ 188 | load: function (filePath, module, moduleFactory) { 189 | var loader = this, 190 | configuredModuleFactory; 191 | 192 | if (loader.initialiserContext.isLoadingBootstraps()) { 193 | module.exports = loader.moduleRepository.loadBootstrap( 194 | filePath, 195 | module.id, 196 | moduleFactory 197 | ); 198 | 199 | return; 200 | } 201 | 202 | configuredModuleFactory = loader.moduleRepository.load( 203 | filePath, 204 | module.id, 205 | moduleFactory 206 | ); 207 | 208 | if (loader.moduleRepository.isLoadingModuleFactoryOnly()) { 209 | module.exports = configuredModuleFactory; 210 | 211 | return; 212 | } 213 | 214 | // A PHP file has been required from JS-land. 215 | module.exports = loader.getEnvironment().requireModule(filePath); 216 | }, 217 | 218 | /** 219 | * Adds files to be stubbed within the virtual filesystem for the environment. 220 | * 221 | * @param {Object.} stubFiles 222 | * @returns {Loader} For chaining 223 | */ 224 | stubFiles: function (stubFiles) { 225 | var loader = this; 226 | 227 | _.forOwn(stubFiles, function (stubFileContents, stubFilePath) { 228 | loader.fileSystem.writeFile(stubFilePath, stubFileContents); 229 | }); 230 | 231 | return loader; 232 | } 233 | }); 234 | 235 | module.exports = Loader; 236 | -------------------------------------------------------------------------------- /src/ModuleRepository.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'), 13 | hasOwn = {}.hasOwnProperty; 14 | 15 | /** 16 | * Contains a cache of configured PHP modules and the module factory fetcher 17 | * to use for both fetching any as-yet unloaded modules and determining their existence 18 | * 19 | * @param {Object} requireCache 20 | * @constructor 21 | */ 22 | function ModuleRepository(requireCache) { 23 | /** 24 | * @type {Object.} PHP module factories, indexed by path 25 | */ 26 | this.configuredModules = {}; 27 | /** 28 | * @type {boolean} Indicates that a module's factory function should be returned without execution. 29 | */ 30 | this.loadingModuleFactoryOnly = false; 31 | /** 32 | * A special function, generated by the PHP module compiler, 33 | * that can be called to fetch the module wrapper of any compiled modules 34 | * or return true/false to determine whether they exist. 35 | * Note that this is set by the special Initialiser module 36 | * generated at compile-time (see Transformer) 37 | * 38 | * @type {Function|null} 39 | */ 40 | this.moduleFactoryFetcher = null; 41 | /** 42 | * @type {Object} require.cache 43 | */ 44 | this.requireCache = requireCache; 45 | } 46 | 47 | _.extend(ModuleRepository.prototype, { 48 | /** 49 | * Fetches the module wrapper factory function for a compiled PHP module, 50 | * if it exists in the compiled bundle 51 | * 52 | * @param {string} filePath 53 | * @returns {Function} 54 | * @throws {Error} Throws when the specified compiled module does not exist 55 | */ 56 | getModuleFactory: function (filePath) { 57 | var configuredModuleFactory, 58 | repository = this; 59 | 60 | if (hasOwn.call(repository.configuredModules, filePath)) { 61 | // Module has already been configured: return the cached module factory 62 | return repository.configuredModules[filePath].factory; 63 | } 64 | 65 | // Module has not yet been loaded: require it via the fetcher function. The transpiled module 66 | // will call back into the loader via `.load(...)` to configure it. 67 | // The `loadingModuleFactoryOnly` flag ensures that the module is not executed, 68 | // only its module factory is exported instead 69 | repository.loadingModuleFactoryOnly = true; 70 | configuredModuleFactory = repository.moduleFactoryFetcher(filePath, false); 71 | repository.loadingModuleFactoryOnly = false; 72 | 73 | if (configuredModuleFactory === null) { 74 | throw new Error('File "' + filePath + '" is not in the compiled PHP file map'); 75 | } 76 | 77 | // By this point, the require()'d module should have called back via .prepare() 78 | // [via .run()] and so its wrapper should be in the map. 79 | if (!hasOwn.call(repository.configuredModules, filePath)) { 80 | throw new Error('Unexpected state: module "' + filePath + '" should have been loaded by now'); 81 | } 82 | if (repository.configuredModules[filePath].factory !== configuredModuleFactory) { 83 | throw new Error('Unexpected state: factory for module "' + filePath + '" loaded incorrectly'); 84 | } 85 | 86 | // Delete the module's exports object from the cache: it was not executed as we only wanted 87 | // to extract the factory function, so that will have been stored instead. 88 | // If the module's factory function is ever needed again, it will be fetched from the 89 | // .configuredModules[...] cache instead. 90 | repository.unrequire(filePath); 91 | 92 | return repository.configuredModules[filePath].factory; 93 | }, 94 | 95 | /** 96 | * Initializes the repository with a function 97 | * for fetching the compiled module wrappers of PHP modules 98 | * 99 | * @param {Function} phpModuleFactoryFetcher 100 | */ 101 | init: function (phpModuleFactoryFetcher) { 102 | this.moduleFactoryFetcher = phpModuleFactoryFetcher; 103 | }, 104 | 105 | /** 106 | * Determines whether a module's factory function should be returned without execution. 107 | * 108 | * @returns {boolean} 109 | */ 110 | isLoadingModuleFactoryOnly: function () { 111 | return this.loadingModuleFactoryOnly; 112 | }, 113 | 114 | /** 115 | * Configures the path for the given module, 116 | * returning the configured module factory. 117 | * 118 | * @param {string} filePath 119 | * @param {string|number} moduleID 120 | * @param {Function} moduleFactory 121 | * @returns {Function} 122 | */ 123 | load: function (filePath, moduleID, moduleFactory) { 124 | var repository = this, 125 | configuredModuleFactory = moduleFactory.using({path: filePath}); 126 | 127 | repository.configuredModules[filePath] = { 128 | id: moduleID, 129 | factory: configuredModuleFactory 130 | }; 131 | 132 | return configuredModuleFactory; 133 | }, 134 | 135 | /** 136 | * Configures the path for the given bootstrap module, 137 | * returning a function that will remove the module from the require cache 138 | * so that it may separately be run as a standalone module if desired, 139 | * and also execute the configured module factory. 140 | * 141 | * @param {string} filePath 142 | * @param {string|number} moduleID 143 | * @param {Function} moduleFactory 144 | * @returns {Function} 145 | */ 146 | loadBootstrap: function (filePath, moduleID, moduleFactory) { 147 | var repository = this, 148 | configuredModuleFactory = moduleFactory.using({path: filePath}); 149 | 150 | repository.configuredModules[filePath] = { 151 | id: moduleID, 152 | factory: configuredModuleFactory 153 | }; 154 | 155 | return function (environment) { 156 | // Remove the module from the require cache so that it may be run separately 157 | // as a standalone module if desired. 158 | repository.unrequire(filePath); 159 | 160 | return configuredModuleFactory({}, environment).execute(); 161 | }; 162 | }, 163 | 164 | /** 165 | * Determines whether the module with the given path exists in the bundle 166 | * 167 | * @param {string} filePath 168 | * @return {boolean} 169 | */ 170 | moduleExists: function (filePath) { 171 | var repository = this; 172 | 173 | if (hasOwn.call(repository.configuredModules, filePath)) { 174 | // Module has already been configured: return the cached module factory 175 | return true; 176 | } 177 | 178 | // Module has not yet been loaded: determine its existence using the fetcher function. 179 | // To save space in the compiled bundle, the large switch statement with a case for each 180 | // compiled PHP module doubles as an existence-check (see Transformer for details) 181 | return repository.moduleFactoryFetcher(filePath, true); 182 | }, 183 | 184 | /** 185 | * Unloads a module from the require cache. 186 | * 187 | * @param {string} filePath 188 | */ 189 | unrequire: function (filePath) { 190 | var cachePath, 191 | configuredModule, 192 | repository = this; 193 | 194 | if (!hasOwn.call(repository.configuredModules, filePath)) { 195 | throw new Error('Module "' + filePath + '" is not loaded'); 196 | } 197 | 198 | cachePath = './' + filePath; 199 | configuredModule = repository.configuredModules[filePath]; 200 | 201 | if (!hasOwn.call(repository.requireCache, configuredModule.id)) { 202 | throw new Error( 203 | 'Path "' + cachePath + '" (id "' + configuredModule.id + '") is not in require.cache' 204 | ); 205 | } 206 | 207 | delete repository.requireCache[configuredModule.id]; 208 | } 209 | }); 210 | 211 | module.exports = ModuleRepository; 212 | -------------------------------------------------------------------------------- /src/Performance.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'); 13 | 14 | /** 15 | * Performance option wrapper 16 | * 17 | * @param {class} Date 18 | * @param {global} global 19 | * @constructor 20 | */ 21 | function Performance(Date, global) { 22 | /** 23 | * @type {class} 24 | */ 25 | this.Date = Date; 26 | /** 27 | * @type {global} 28 | */ 29 | this.global = global; 30 | } 31 | 32 | _.extend(Performance.prototype, { 33 | /** 34 | * Returns the time since the Unix epoch in microseconds 35 | * 36 | * @returns {number} 37 | */ 38 | getTimeInMicroseconds: function () { 39 | var performance = this; 40 | 41 | if (performance.global.performance) { 42 | // Use 5-microsecond-precise Performance API, if available 43 | return ( 44 | performance.global.performance.timing.navigationStart + performance.global.performance.now() 45 | ) * 1000; 46 | } 47 | 48 | // Fall back to fake microsecond accuracy (will be correct to the nearest millisecond) 49 | return new performance.Date().getTime() * 1000; 50 | } 51 | }); 52 | 53 | module.exports = Performance; 54 | -------------------------------------------------------------------------------- /src/Transformer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('microdash'), 13 | BOOTSTRAPS = 'bootstraps', 14 | INCLUDE = 'include', 15 | MODE = 'mode', 16 | STUB = 'stub', 17 | STUB_FILES = 'stubFiles', 18 | SYNC = 'sync', 19 | hasOwn = {}.hasOwnProperty, 20 | nowdoc = require('nowdoc'), 21 | path = require('path'); 22 | 23 | /** 24 | * Transforms a PHP module to a CommonJS module suitable for bundling for the browser or Node.js 25 | * e.g. with Browserify or Webpack. 26 | * 27 | * @param {Object} phpParser 28 | * @param {Object} phpToJS 29 | * @param {Function} resolveRequire 30 | * @param {Object} globby 31 | * @param {string} initialiserStubPath 32 | * @param {Object} phpifyConfig 33 | * @param {Object} phpToJSConfig 34 | * @param {Object} transpilerConfig 35 | * @param {LibraryConfigShape} phpCoreConfig 36 | * @param {string} contextDirectory 37 | * @constructor 38 | */ 39 | function Transformer( 40 | phpParser, 41 | phpToJS, 42 | resolveRequire, 43 | globby, 44 | initialiserStubPath, 45 | phpifyConfig, 46 | phpToJSConfig, 47 | transpilerConfig, 48 | phpCoreConfig, 49 | contextDirectory 50 | ) { 51 | /** 52 | * @type {string} 53 | */ 54 | this.contextDirectory = contextDirectory; 55 | /** 56 | * @type {Object} 57 | */ 58 | this.globby = globby; 59 | /** 60 | * @type {string} 61 | */ 62 | this.initialiserStubPath = initialiserStubPath; 63 | /** 64 | * @type {LibraryConfigShape} 65 | */ 66 | this.phpCoreConfig = phpCoreConfig; 67 | /** 68 | * @type {Object} 69 | */ 70 | this.phpifyConfig = phpifyConfig; 71 | /** 72 | * @type {Object} 73 | */ 74 | this.phpParser = phpParser; 75 | /** 76 | * @type {Object} 77 | */ 78 | this.phpToJS = phpToJS; 79 | /** 80 | * @type {Object} 81 | */ 82 | this.phpToJSConfig = phpToJSConfig; 83 | /** 84 | * @type {Function} 85 | */ 86 | this.resolveRequire = resolveRequire; 87 | /** 88 | * @type {Object} 89 | */ 90 | this.transpilerConfig = transpilerConfig; 91 | } 92 | 93 | _.extend(Transformer.prototype, { 94 | /** 95 | * Transforms the specified PHP module code to a CommonJS module 96 | * 97 | * @param {string} content 98 | * @param {string} file 99 | * @returns {{code: string, map: object}} 100 | */ 101 | transform: function (content, file) { 102 | var transformer = this, 103 | mode = transformer.phpToJSConfig[SYNC] === true ? 104 | 'sync' : 105 | (transformer.phpToJSConfig[MODE] || 'async'), 106 | apiPath = path.dirname(transformer.resolveRequire('phpify')) + 107 | '/api' + 108 | (mode === 'async' ? '' : '/' + mode), 109 | stubs = transformer.phpifyConfig[STUB] || {}, 110 | prefixJS, 111 | relativeFilePath = path.relative(transformer.contextDirectory, file), 112 | runtimePath = path.dirname(transformer.resolveRequire('phpruntime')), 113 | stub, 114 | suffixJS; 115 | 116 | /** 117 | * Performs the actual compilation of a module, unless it was the initialiser module. 118 | * 119 | * @param {string} content 120 | * @param {string} filePath 121 | * @param {string} prefix 122 | * @param {string} suffix 123 | * @return {{code: string, map: Object}} CommonJS output from PHPToJS and PHP->JS source map data 124 | */ 125 | function compileModule(content, filePath, prefix, suffix) { 126 | var phpAST; 127 | 128 | // Tell the parser the path to the current file 129 | // so it can be included in error messages. 130 | transformer.phpParser.getState().setPath(filePath); 131 | 132 | phpAST = transformer.phpParser.parse(content); 133 | 134 | return transformer.phpToJS.transpile( 135 | phpAST, 136 | _.extend( 137 | { 138 | 'path': filePath, 139 | 'runtimePath': runtimePath, 140 | 'prefix': prefix, 141 | 'suffix': suffix, 142 | 'sourceMap': { 143 | // Keep the source map data as a separate object and return it to us, 144 | // rather than generating a source map comment with it inside, 145 | // so that we can much more efficiently just pass it along to Webpack (for example). 146 | 'returnMap': true, 147 | 148 | 'sourceContent': content 149 | } 150 | }, 151 | transformer.phpToJSConfig 152 | ), 153 | // Any custom rules etc. will need to be specified here instead. 154 | transformer.transpilerConfig 155 | ); 156 | } 157 | 158 | function buildStubFiles() { 159 | var stubFiles = transformer.phpifyConfig[STUB_FILES] || {}, 160 | mappedStubFiles = {}; 161 | 162 | if (Object.keys(stubFiles).length === 0) { 163 | return null; 164 | } 165 | 166 | _.forOwn(stubFiles, function (stubFileContents, stubFilePath) { 167 | // Stub file paths should be resolved relative to the initialiser, 168 | // so that the compiled bundle does not contain absolute paths. 169 | var initialiserRelativeStubFilePath = path.relative( 170 | transformer.contextDirectory, 171 | path.resolve(transformer.contextDirectory, stubFilePath) 172 | ); 173 | 174 | mappedStubFiles[initialiserRelativeStubFilePath] = stubFileContents; 175 | }); 176 | 177 | return mappedStubFiles; 178 | } 179 | 180 | /** 181 | * The initialiser is required by all modules. It will only execute once (as with all 182 | * CommonJS modules that are not cleared from require.cache) but configures the runtime 183 | * with the virtual FS containing all bundled PHP modules and any bootstraps. 184 | * 185 | * @return {string} 186 | */ 187 | function buildInitialiser() { 188 | var bootstraps = transformer.phpifyConfig[BOOTSTRAPS] || [], 189 | globPaths = _.map(transformer.phpifyConfig[INCLUDE] || [], function (path) { 190 | if (/^!/.test(path)) { 191 | // Keep the exclamation mark (which marks paths to exclude) 192 | // at the beginning of the string 193 | return '!' + transformer.contextDirectory + '/' + path.substr(1); 194 | } 195 | 196 | return transformer.contextDirectory + '/' + path; 197 | }), 198 | files = transformer.globby.sync(globPaths), 199 | phpModuleFactories = [], 200 | stubFiles = buildStubFiles(); 201 | 202 | _.each(files, function (filePath) { 203 | var contextRelativePath = path.relative(transformer.contextDirectory, filePath), 204 | // `./` is required for Browserify/Webpack to correctly resolve relative paths - 205 | // paths starting with no dot or slash, e.g. `Demo/file.php` were not being found. 206 | initialiserRelativePath = './' + path.relative(path.dirname(file), filePath); 207 | 208 | phpModuleFactories.push( 209 | 'case handlePath(' + JSON.stringify(contextRelativePath) + '): ' + 210 | 'return require(' + JSON.stringify(initialiserRelativePath) + ');' 211 | ); 212 | }); 213 | 214 | return nowdoc(function () {/*<< 0 ? 246 | '\n .bootstrap(function () { return [' + 247 | bootstraps 248 | .map(function (bootstrapPath) { 249 | // Bootstrap paths should be resolved relative to the initialiser, 250 | // so that the compiled bundle does not contain absolute paths. 251 | var initialiserRelativeBootstrapPath = './' + path.relative( 252 | path.dirname(file), 253 | path.resolve(transformer.contextDirectory, bootstrapPath) 254 | ); 255 | 256 | // NB: ./ is required by bundlers. 257 | return 'require(' + JSON.stringify(initialiserRelativeBootstrapPath) + ')'; 258 | }) 259 | .join(', ') + 260 | ']; })' : 261 | '', 262 | configureCall: '\n .configure(' + 263 | JSON.stringify({ 264 | stdio: transformer.phpifyConfig.stdio !== false 265 | }) + 266 | ', [' + 267 | transformer.phpCoreConfig.pluginConfigFilePaths 268 | .map(function (path) { 269 | return 'require(' + JSON.stringify(path) + ')'; 270 | }) 271 | .concat([JSON.stringify(transformer.phpCoreConfig.topLevelConfig)]) 272 | .join(', ') + 273 | '])', 274 | // Optionally add a call to Loader.stubFiles(...) to install the stub files 275 | // if any have been specified. 276 | stubFilesCall: stubFiles !== null ? 277 | '\n .stubFiles(' + JSON.stringify(stubFiles) + ')' : 278 | '', 279 | switchCases: phpModuleFactories.join('\n ') 280 | }); 281 | } 282 | 283 | if (file === transformer.initialiserStubPath) { 284 | // The included module is the initialiser: output the virtual FS switch() and other config 285 | // as its only contents. It will be required by every other transformed PHP file, 286 | // so that they have access to the virtual FS (see below). 287 | // Doing it this way keeps the transformer stateless, which is needed for HappyPack support. 288 | return { 289 | code: buildInitialiser(), 290 | 291 | // No source map to return for the initialiser stub. 292 | map: null 293 | }; 294 | } 295 | 296 | if (hasOwn.call(stubs, relativeFilePath)) { 297 | stub = stubs[relativeFilePath]; 298 | 299 | if (typeof stub === 'string') { 300 | // String values provide some raw PHP source code for the stub. 301 | content = stub; 302 | } else if (typeof stub === 'boolean' || typeof stub === 'number' || stub === null) { 303 | // Primitive values provide a literal value for the module to return. 304 | content = ' { 150 | resolveBootstrap1 = resolve; 151 | }); 152 | let bootstrap2Promise = new Promise(resolve => { 153 | resolveBootstrap2 = resolve; 154 | }); 155 | 156 | asyncBootstrap1.returns(bootstrap1Promise); 157 | asyncBootstrap2.returns(bootstrap2Promise); 158 | 159 | let requirePromise = environment.requireModule('/path/to/module.php'); 160 | 161 | expect(asyncBootstrap1).to.have.been.calledOnce; 162 | expect(asyncBootstrap1).to.have.been.calledWith(sinon.match.same(phpCoreEnvironment)); 163 | expect(asyncBootstrap2).not.to.have.been.called; 164 | resolveBootstrap1(); 165 | await Promise.resolve(); // Allow promise resolution microtask to run. 166 | expect(asyncBootstrap2).to.have.been.calledOnce; 167 | expect(asyncBootstrap2).to.have.been.calledWith(sinon.match.same(phpCoreEnvironment)); 168 | resolveBootstrap2(); 169 | await requirePromise; 170 | expect(asyncBootstrap2).to.have.been.calledAfter(asyncBootstrap1); 171 | }); 172 | 173 | it('should execute the module after all bootstraps are complete', async function () { 174 | let bootstrap1Promise = new Promise(resolve => { 175 | resolveBootstrap1 = resolve; 176 | }); 177 | 178 | asyncBootstrap1.returns(bootstrap1Promise); 179 | asyncBootstrap2.returns(null); // Synchronous bootstrap. 180 | 181 | let requirePromise = environment.requireModule('/path/to/module.php'); 182 | 183 | expect(moduleFactory).not.to.have.been.called; 184 | resolveBootstrap1(); 185 | await requirePromise; 186 | expect(moduleFactory).to.have.been.calledOnce; 187 | expect(moduleFactory).to.have.been.calledWith({}, sinon.match.same(phpCoreEnvironment)); 188 | expect(moduleExecutable.execute).to.have.been.calledOnce; 189 | }); 190 | 191 | it('should not load the PHP module following an error during bootstrap', async function () { 192 | const testError = new Error('Bootstrap error'); 193 | asyncBootstrap1.returns(Promise.reject(testError)); 194 | 195 | const requirePromise = environment.requireModule('/path/to/module.php'); 196 | 197 | await expect(requirePromise).to.be.rejectedWith(testError); 198 | expect(moduleFactory).not.to.have.been.called; 199 | }); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /test/unit/FileSystemTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var expect = require('chai').expect, 13 | sinon = require('sinon'), 14 | FileSystem = require('../../src/FileSystem'), 15 | ModuleRepository = require('../../src/ModuleRepository'); 16 | 17 | describe('FileSystem', function () { 18 | var fileSystem, 19 | moduleRepository; 20 | 21 | beforeEach(function () { 22 | moduleRepository = sinon.createStubInstance(ModuleRepository); 23 | 24 | fileSystem = new FileSystem(moduleRepository); 25 | }); 26 | 27 | describe('getModuleFactory()', function () { 28 | it('should return the module factory from the repository after resolving the path', function () { 29 | var moduleFactory = sinon.stub(); 30 | moduleRepository.getModuleFactory 31 | .withArgs('my/module/path.php') 32 | .returns(moduleFactory); 33 | 34 | expect(fileSystem.getModuleFactory('my/module/in/here/../../path.php')).to.equal(moduleFactory); 35 | }); 36 | }); 37 | 38 | describe('isDirectory()', function () { 39 | it('should always return false for now', function () { 40 | expect(fileSystem.isDirectory('/my/dir/path')).to.be.false; 41 | }); 42 | }); 43 | 44 | describe('isFile()', function () { 45 | it('should return true when a PHP module exists with the given path', function () { 46 | moduleRepository.moduleExists 47 | .withArgs('my/module/path.php') 48 | .returns(true); 49 | 50 | expect(fileSystem.isFile('my/module/in/here/../../path.php')).to.be.true; 51 | }); 52 | 53 | it('should return false when no PHP module exists with the given path', function () { 54 | moduleRepository.moduleExists 55 | .withArgs('my/module/path.php') 56 | .returns(false); 57 | 58 | expect(fileSystem.isFile('my/module/in/here/../../path.php')).to.be.false; 59 | }); 60 | }); 61 | 62 | describe('open()', function () { 63 | it('should be rejected as Streams are not supported', function () { 64 | expect(fileSystem.open('/my/file.txt')).to.be.rejectedWith( 65 | 'Could not open "/my/file.txt" :: Streams are not currently supported by PHPify' 66 | ); 67 | }); 68 | }); 69 | 70 | describe('openSync()', function () { 71 | it('should throw as Streams are not supported', function () { 72 | expect(function () { 73 | fileSystem.openSync('/my/file.txt'); 74 | }).to.throw( 75 | 'Could not open "/my/file.txt" :: Streams are not currently supported by PHPify' 76 | ); 77 | }); 78 | }); 79 | 80 | describe('realPath()', function () { 81 | it('should resolve any parent directory symbols in the path', function () { 82 | expect(fileSystem.realPath('my/path/../to/a/../mod/u/le/../../file.js')) 83 | .to.equal('my/to/mod/file.js'); 84 | }); 85 | 86 | it('should strip any leading forward-slash', function () { 87 | expect(fileSystem.realPath('/my/path/../to/a/../mod/u/le/../../file.js')) 88 | .to.equal('my/to/mod/file.js'); 89 | }); 90 | }); 91 | 92 | describe('unlink()', function () { 93 | it('should be rejected as file and folder deletion is currently not supported', function () { 94 | expect(fileSystem.unlink('/my/file.txt')).to.be.rejectedWith( 95 | 'Could not delete "/my/file.txt" :: not currently supported by PHPify' 96 | ); 97 | }); 98 | }); 99 | 100 | describe('unlinkSync()', function () { 101 | it('should throw as file and folder deletion is currently not supported', function () { 102 | expect(function () { 103 | fileSystem.unlinkSync('/my/file.txt'); 104 | }).to.throw( 105 | 'Could not delete "/my/file.txt" :: not currently supported by PHPify' 106 | ); 107 | }); 108 | }); 109 | 110 | describe('writeFile()', function () { 111 | it('should allow files to be detected by isFile()', function () { 112 | fileSystem.writeFile('my/file.txt', 'contents'); 113 | 114 | expect(fileSystem.isFile('my/file.txt')).to.be.true; 115 | }); 116 | 117 | it('should allow files to be overwritten and still detected', function () { 118 | fileSystem.writeFile('my/file.txt', 'original contents'); 119 | fileSystem.writeFile('my/file.txt', 'new contents'); 120 | 121 | expect(fileSystem.isFile('my/file.txt')).to.be.true; 122 | }); 123 | 124 | it('should not affect detection of PHP modules', function () { 125 | moduleRepository.moduleExists 126 | .withArgs('my/module.php') 127 | .returns(true); 128 | 129 | fileSystem.writeFile('my/module.php', 'some contents'); 130 | 131 | expect(fileSystem.isFile('my/module.php')).to.be.true; 132 | }); 133 | 134 | it('should not affect detection of non-existent files in other paths', function () { 135 | moduleRepository.moduleExists 136 | .withArgs('nonexistent/file.txt') 137 | .returns(false); 138 | 139 | fileSystem.writeFile('my/file.txt', 'contents'); 140 | 141 | expect(fileSystem.isFile('nonexistent/file.txt')).to.be.false; 142 | }); 143 | 144 | it('should not affect detection of non-existent files in parent directories', function () { 145 | moduleRepository.moduleExists 146 | .withArgs('../other/file.txt') 147 | .returns(false); 148 | 149 | fileSystem.writeFile('my/file.txt', 'contents'); 150 | 151 | expect(fileSystem.isFile('../other/file.txt')).to.be.false; 152 | }); 153 | 154 | it('should not affect detection of non-existent PHP modules', function () { 155 | moduleRepository.moduleExists 156 | .withArgs('nonexistent/module.php') 157 | .returns(false); 158 | 159 | fileSystem.writeFile('my/other.php', 'some contents'); 160 | 161 | expect(fileSystem.isFile('nonexistent/module.php')).to.be.false; 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /test/unit/IOTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var expect = require('chai').expect, 13 | sinon = require('sinon'), 14 | EventEmitter = require('events').EventEmitter, 15 | IO = require('../../src/IO'); 16 | 17 | describe('IO', function () { 18 | var console, 19 | environment, 20 | io, 21 | phpifyConfig, 22 | phpStderr, 23 | phpStdout; 24 | 25 | beforeEach(function () { 26 | console = { 27 | info: sinon.stub(), 28 | warn: sinon.stub() 29 | }; 30 | phpifyConfig = { 31 | stdio: true 32 | }; 33 | phpStderr = new EventEmitter(); 34 | phpStdout = new EventEmitter(); 35 | environment = { 36 | getStderr: sinon.stub().returns(phpStderr), 37 | getStdout: sinon.stub().returns(phpStdout) 38 | }; 39 | 40 | io = new IO(console); 41 | }); 42 | 43 | describe('install()', function () { 44 | describe('when the console is available and stdio is enabled', function () { 45 | it('should install a listener that copies data from the PHP stdout to the console', function () { 46 | io.install(environment, phpifyConfig); 47 | 48 | phpStdout.emit('data', 'some output'); 49 | 50 | expect(console.info).to.have.been.calledOnce; 51 | expect(console.info).to.have.been.calledWith('some output'); 52 | expect(console.warn).not.to.have.been.called; 53 | }); 54 | 55 | it('should install a listener that copies data from the PHP stderr to the console', function () { 56 | io.install(environment, phpifyConfig); 57 | 58 | phpStderr.emit('data', 'Bang! Something went wrong'); 59 | 60 | expect(console.warn).to.have.been.calledOnce; 61 | expect(console.warn).to.have.been.calledWith('Bang! Something went wrong'); 62 | expect(console.info).not.to.have.been.called; 63 | }); 64 | 65 | it('should install a stdout listener onto each different one provided', function () { 66 | var secondPhpStderr = new EventEmitter(), 67 | secondPhpStdout = new EventEmitter(), 68 | secondEnvironment = { 69 | getStdout: sinon.stub().returns(secondPhpStdout), 70 | getStderr: sinon.stub().returns(secondPhpStderr), 71 | }; 72 | io.install(environment, phpifyConfig); 73 | io.install(secondEnvironment, phpifyConfig); 74 | 75 | phpStdout.emit('data', 'first output'); 76 | secondPhpStdout.emit('data', 'second output'); 77 | 78 | expect(console.info).to.have.been.calledTwice; 79 | expect(console.info).to.have.been.calledWith('first output'); 80 | expect(console.info).to.have.been.calledWith('second output'); 81 | expect(console.warn).not.to.have.been.called; 82 | }); 83 | 84 | it('should install a stderr listener onto each different one provided', function () { 85 | var secondPhpStderr = new EventEmitter(), 86 | secondPhpStdout = new EventEmitter(), 87 | secondEnvironment = { 88 | getStdout: sinon.stub().returns(secondPhpStdout), 89 | getStderr: sinon.stub().returns(secondPhpStderr), 90 | }; 91 | io.install(environment, phpifyConfig); 92 | io.install(secondEnvironment, phpifyConfig); 93 | 94 | phpStderr.emit('data', 'first warning'); 95 | secondPhpStderr.emit('data', 'second warning'); 96 | 97 | expect(console.warn).to.have.been.calledTwice; 98 | expect(console.warn).to.have.been.calledWith('first warning'); 99 | expect(console.warn).to.have.been.calledWith('second warning'); 100 | expect(console.info).not.to.have.been.called; 101 | }); 102 | }); 103 | 104 | describe('when the console is unavailable', function () { 105 | beforeEach(function () { 106 | io = new IO(null); 107 | }); 108 | 109 | it('should not install a listener that copies data from the PHP stdout', function () { 110 | io.install(environment, phpifyConfig); 111 | 112 | phpStdout.emit('data', 'some output'); 113 | 114 | expect(console.info).not.to.have.been.called; 115 | expect(console.warn).not.to.have.been.called; 116 | }); 117 | 118 | it('should install a listener that copies data from the PHP stderr', function () { 119 | io.install(environment, phpifyConfig); 120 | 121 | phpStderr.emit('data', 'Bang! Something went wrong'); 122 | 123 | expect(console.info).not.to.have.been.called; 124 | expect(console.warn).not.to.have.been.called; 125 | }); 126 | }); 127 | 128 | describe('when stdio is disabled in config', function () { 129 | beforeEach(function () { 130 | phpifyConfig.stdio = false; 131 | }); 132 | 133 | it('should not install a listener that copies data from the PHP stdout', function () { 134 | io.install(environment, phpifyConfig); 135 | 136 | phpStdout.emit('data', 'some output'); 137 | 138 | expect(console.info).not.to.have.been.called; 139 | expect(console.warn).not.to.have.been.called; 140 | }); 141 | 142 | it('should not install a listener that copies data from the PHP stderr', function () { 143 | io.install(environment, phpifyConfig); 144 | 145 | phpStderr.emit('data', 'Bang! Something went wrong'); 146 | 147 | expect(console.info).not.to.have.been.called; 148 | expect(console.warn).not.to.have.been.called; 149 | }); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/unit/Initialiser/InitialiserContextTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var expect = require('chai').expect, 13 | sinon = require('sinon'), 14 | InitialiserContext = require('../../../src/Initialiser/InitialiserContext'); 15 | 16 | describe('InitialiserContext', function () { 17 | var context; 18 | 19 | beforeEach(function () { 20 | context = new InitialiserContext(); 21 | }); 22 | 23 | describe('bootstrap()', function () { 24 | it('should store the bootstrap fetcher function', function () { 25 | var bootstrapFetcher = sinon.stub(); 26 | 27 | context.bootstrap(bootstrapFetcher); 28 | 29 | expect(context.bootstrapFetcher).to.equal(bootstrapFetcher); 30 | }); 31 | }); 32 | 33 | describe('getBootstraps()', function () { 34 | it('should return an empty array when no bootstrap fetcher is set', function () { 35 | var result = context.getBootstraps(); 36 | 37 | expect(result).to.deep.equal([]); 38 | }); 39 | 40 | it('should return the bootstraps from the bootstrap fetcher', function () { 41 | var bootstraps = ['bootstrap1', 'bootstrap2'], 42 | bootstrapFetcher = sinon.stub().returns(bootstraps), 43 | result; 44 | 45 | context.bootstrap(bootstrapFetcher); 46 | result = context.getBootstraps(); 47 | 48 | expect(bootstrapFetcher).to.have.been.calledOnce; 49 | expect(result).to.equal(bootstraps); 50 | }); 51 | 52 | it('should set loadingBootstraps flag to true while fetching bootstraps', function () { 53 | var wasLoadingBootstraps = false, 54 | bootstrapFetcher = sinon.stub().callsFake(function () { 55 | wasLoadingBootstraps = context.loadingBootstraps; 56 | return []; 57 | }); 58 | 59 | context.bootstrap(bootstrapFetcher); 60 | context.getBootstraps(); 61 | 62 | expect(wasLoadingBootstraps).to.be.true; 63 | expect(context.loadingBootstraps).to.be.false; 64 | }); 65 | }); 66 | 67 | describe('isLoadingBootstraps()', function () { 68 | it('should return false by default', function () { 69 | expect(context.isLoadingBootstraps()).to.be.false; 70 | }); 71 | 72 | it('should return true while bootstraps are being loaded', function () { 73 | var wasLoadingBootstraps = false, 74 | bootstrapFetcher = sinon.stub().callsFake(function () { 75 | wasLoadingBootstraps = context.isLoadingBootstraps(); 76 | return []; 77 | }); 78 | 79 | context.bootstrap(bootstrapFetcher); 80 | context.getBootstraps(); 81 | 82 | expect(wasLoadingBootstraps).to.be.true; 83 | expect(context.isLoadingBootstraps()).to.be.false; 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/unit/Initialiser/InitialiserLoaderTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var expect = require('chai').expect, 13 | sinon = require('sinon'), 14 | InitialiserLoader = require('../../../src/Initialiser/InitialiserLoader'); 15 | 16 | describe('InitialiserLoader', function () { 17 | var initialiser, 18 | initialiserLoader, 19 | initialiserRequirer; 20 | 21 | beforeEach(function () { 22 | initialiser = function () {}; 23 | initialiserRequirer = sinon.stub().returns(initialiser); 24 | initialiserLoader = new InitialiserLoader(initialiserRequirer); 25 | }); 26 | 27 | describe('loadInitialiser()', function () { 28 | it('should call the initialiser requirer function', function () { 29 | initialiserLoader.loadInitialiser(); 30 | 31 | expect(initialiserRequirer).to.have.been.calledOnce; 32 | }); 33 | 34 | it('should return the result of the initialiser requirer', function () { 35 | expect(initialiserLoader.loadInitialiser()).to.equal(initialiser); 36 | }); 37 | }); 38 | 39 | describe('setRequirer()', function () { 40 | it('should replace the initialiser requirer', function () { 41 | var newInitialiser = function () {}, 42 | newInitialiserRequirer = sinon.stub().returns(newInitialiser), 43 | result; 44 | 45 | initialiserLoader.setRequirer(newInitialiserRequirer); 46 | result = initialiserLoader.loadInitialiser(); 47 | 48 | expect(initialiserRequirer).not.to.have.been.called; 49 | expect(newInitialiserRequirer).to.have.been.calledOnce; 50 | expect(result).to.equal(newInitialiser); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/unit/LoaderTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var expect = require('chai').expect, 13 | sinon = require('sinon'), 14 | Environment = require('../../src/Environment/Environment'), 15 | EnvironmentProvider = require('../../src/Environment/EnvironmentProvider'), 16 | FileSystem = require('../../src/FileSystem'), 17 | InitialiserContext = require('../../src/Initialiser/InitialiserContext'), 18 | InitialiserLoader = require('../../src/Initialiser/InitialiserLoader'), 19 | Loader = require('../../src/Loader'), 20 | ModuleRepository = require('../../src/ModuleRepository'); 21 | 22 | describe('Loader', function () { 23 | var environment, 24 | environmentProvider, 25 | fileSystem, 26 | initialiser, 27 | initialiserContext, 28 | initialiserLoader, 29 | loader, 30 | mergeAllResult, 31 | moduleRepository, 32 | phpConfigImporter; 33 | 34 | beforeEach(function () { 35 | environment = sinon.createStubInstance(Environment); 36 | environmentProvider = sinon.createStubInstance(EnvironmentProvider); 37 | fileSystem = sinon.createStubInstance(FileSystem); 38 | initialiser = sinon.stub(); 39 | initialiserContext = sinon.createStubInstance(InitialiserContext); 40 | initialiserLoader = sinon.createStubInstance(InitialiserLoader); 41 | mergeAllResult = { 42 | my: 'PHPCore config', 43 | addons: ['base-addon'] 44 | }; 45 | moduleRepository = sinon.createStubInstance(ModuleRepository); 46 | phpConfigImporter = { 47 | importLibrary: sinon.stub().returns({ 48 | mergeAll: sinon.stub().returns(mergeAllResult) 49 | }) 50 | }; 51 | 52 | environment.requireModule.returns('my module execution result'); 53 | environmentProvider.createEnvironment.returns(environment); 54 | initialiserLoader.loadInitialiser.returns(initialiser); 55 | 56 | loader = new Loader( 57 | moduleRepository, 58 | initialiserContext, 59 | fileSystem, 60 | environmentProvider, 61 | phpConfigImporter 62 | ); 63 | }); 64 | 65 | describe('bootstrap()', function () { 66 | it('should register the bootstrap fetcher with the InitialiserContext', function () { 67 | var bootstrapFetcher = sinon.stub(); 68 | 69 | loader.bootstrap(bootstrapFetcher); 70 | 71 | expect(initialiserContext.bootstrap).to.have.been.calledOnce; 72 | expect(initialiserContext.bootstrap).to.have.been.calledWith(sinon.match.same(bootstrapFetcher)); 73 | }); 74 | 75 | it('should return the Loader for chaining', function () { 76 | expect(loader.bootstrap(sinon.stub())).to.equal(loader); 77 | }); 78 | }); 79 | 80 | describe('configure()', function () { 81 | it('should import the PHPCore library config correctly', function () { 82 | var config1 = {}, 83 | config2 = {}; 84 | 85 | loader.configure({}, [config1, config2]); 86 | 87 | expect(phpConfigImporter.importLibrary).to.have.been.calledOnce; 88 | expect(phpConfigImporter.importLibrary).to.have.been.calledWith( 89 | {configs: [config1, config2]} 90 | ); 91 | }); 92 | 93 | it('should return the Loader for chaining', function () { 94 | expect(loader.configure({}, [])).to.equal(loader); 95 | }); 96 | }); 97 | 98 | describe('createEnvironment()', function () { 99 | it('should pass the ModuleRepository to the EnvironmentProvider', function () { 100 | loader.createEnvironment(); 101 | 102 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 103 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 104 | sinon.match.same(moduleRepository) 105 | ); 106 | }); 107 | 108 | it('should pass the InitialiserContext to the EnvironmentProvider', function () { 109 | loader.createEnvironment(); 110 | 111 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 112 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 113 | sinon.match.any, 114 | sinon.match.same(initialiserContext) 115 | ); 116 | }); 117 | 118 | it('should pass the FileSystem to the EnvironmentProvider', function () { 119 | loader.createEnvironment(); 120 | 121 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 122 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 123 | sinon.match.any, 124 | sinon.match.any, 125 | sinon.match.same(fileSystem) 126 | ); 127 | }); 128 | 129 | it('should pass the PHPify config that was passed to .configure()', function () { 130 | loader.configure({my: 'PHPify config'}); 131 | 132 | loader.createEnvironment(); 133 | 134 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 135 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 136 | sinon.match.any, 137 | sinon.match.any, 138 | sinon.match.any, 139 | {my: 'PHPify config'} 140 | ); 141 | }); 142 | 143 | it('should pass the PHPCore config that was passed to .configure()', function () { 144 | loader.configure({}, [{first: 'PHPCore config 1'}, {second: 'PHPCore config 2'}]); 145 | 146 | loader.createEnvironment(); 147 | 148 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 149 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 150 | sinon.match.any, 151 | sinon.match.any, 152 | sinon.match.any, 153 | sinon.match.any, 154 | sinon.match({ 155 | my: 'PHPCore config' 156 | }) 157 | ); 158 | }); 159 | 160 | it('should merge additional PHPify config when provided', function () { 161 | loader.configure({base: 'config'}); 162 | 163 | loader.createEnvironment(null, {additional: 'config'}); 164 | 165 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 166 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 167 | sinon.match.any, 168 | sinon.match.any, 169 | sinon.match.any, 170 | sinon.match({ 171 | base: 'config', 172 | additional: 'config' 173 | }) 174 | ); 175 | }); 176 | 177 | it('should merge additional PHPCore config when provided', function () { 178 | loader.configure({}, [{base: 'config'}]); 179 | 180 | loader.createEnvironment({additional: 'config'}); 181 | 182 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 183 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 184 | sinon.match.any, 185 | sinon.match.any, 186 | sinon.match.any, 187 | sinon.match.any, 188 | sinon.match({ 189 | my: 'PHPCore config', 190 | additional: 'config' 191 | }) 192 | ); 193 | }); 194 | 195 | it('should merge addons arrays when provided', function () { 196 | loader.configure({}, [{addons: ['base-addon']}]); 197 | 198 | loader.createEnvironment({addons: ['additional-addon']}); 199 | 200 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 201 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 202 | sinon.match.any, 203 | sinon.match.any, 204 | sinon.match.any, 205 | sinon.match.any, 206 | sinon.match({ 207 | my: 'PHPCore config', 208 | addons: ['base-addon', 'additional-addon'] 209 | }) 210 | ); 211 | }); 212 | 213 | it('should return the created Environment', function () { 214 | expect(loader.createEnvironment()).to.equal(environment); 215 | }); 216 | }); 217 | 218 | describe('getEnvironment()', function () { 219 | it('should pass the ModuleRepository to the EnvironmentProvider', function () { 220 | loader.getEnvironment(); 221 | 222 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 223 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 224 | sinon.match.same(moduleRepository) 225 | ); 226 | }); 227 | 228 | it('should pass the InitialiserContext to the EnvironmentProvider', function () { 229 | loader.getEnvironment(); 230 | 231 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 232 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 233 | sinon.match.any, 234 | sinon.match.same(initialiserContext) 235 | ); 236 | }); 237 | 238 | it('should pass the FileSystem to the EnvironmentProvider', function () { 239 | loader.getEnvironment(); 240 | 241 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 242 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 243 | sinon.match.any, 244 | sinon.match.any, 245 | sinon.match.same(fileSystem) 246 | ); 247 | }); 248 | 249 | it('should pass the PHPify config that was passed to .configure()', function () { 250 | loader.configure({my: 'PHPify config'}); 251 | 252 | loader.getEnvironment(); 253 | 254 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 255 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 256 | sinon.match.any, 257 | sinon.match.any, 258 | sinon.match.any, 259 | {my: 'PHPify config'} 260 | ); 261 | }); 262 | 263 | it('should pass the PHPCore config that was passed to .configure()', function () { 264 | loader.configure({}, [{first: 'PHPCore config 1'}, {second: 'PHPCore config 2'}]); 265 | 266 | loader.getEnvironment(); 267 | 268 | expect(environmentProvider.createEnvironment).to.have.been.calledOnce; 269 | expect(environmentProvider.createEnvironment).to.have.been.calledWith( 270 | sinon.match.any, 271 | sinon.match.any, 272 | sinon.match.any, 273 | sinon.match.any, 274 | sinon.match({ 275 | my: 'PHPCore config' 276 | }) 277 | ); 278 | }); 279 | }); 280 | 281 | describe('getModuleFactory()', function () { 282 | it('should return the module factory from the ModuleRepository', function () { 283 | var moduleFactory = sinon.stub(); 284 | moduleRepository.getModuleFactory 285 | .withArgs('/my/php/file/path.php') 286 | .returns(moduleFactory); 287 | 288 | expect(loader.getModuleFactory('/my/php/file/path.php')).to.equal(moduleFactory); 289 | }); 290 | }); 291 | 292 | describe('installModules()', function () { 293 | var moduleFactoryFetcher; 294 | 295 | beforeEach(function () { 296 | moduleFactoryFetcher = sinon.stub(); 297 | }); 298 | 299 | it('should initialize the ModuleRepository', function () { 300 | loader.installModules(moduleFactoryFetcher); 301 | 302 | expect(moduleRepository.init).to.have.been.calledOnce; 303 | }); 304 | 305 | it('should pass the module factory fetcher through to the ModuleRepository', function () { 306 | loader.installModules(moduleFactoryFetcher); 307 | 308 | expect(moduleRepository.init).to.have.been.calledWith(sinon.match.same(moduleFactoryFetcher)); 309 | }); 310 | 311 | it('should return the Loader for chaining', function () { 312 | expect(loader.installModules(moduleFactoryFetcher)).to.equal(loader); 313 | }); 314 | }); 315 | 316 | describe('isInitialised()', function () { 317 | it('should return false initially', function () { 318 | expect(loader.isInitialised()).to.be.false; 319 | }); 320 | 321 | it('should return true after the Environment has been created', function () { 322 | loader.getEnvironment(); 323 | 324 | expect(loader.isInitialised()).to.be.true; 325 | }); 326 | }); 327 | 328 | describe('load()', function () { 329 | beforeEach(function () { 330 | initialiserContext.isLoadingBootstraps.returns(false); 331 | moduleRepository.isLoadingModuleFactoryOnly.returns(false); 332 | }); 333 | 334 | it('should use the Environment to require the module', function () { 335 | var module = {exports: null, id: 'my-module'}; 336 | 337 | loader.load('my/module/path.php', module); 338 | 339 | expect(environment.requireModule).to.have.been.calledOnce; 340 | expect(environment.requireModule).to.have.been.calledWith('my/module/path.php'); 341 | }); 342 | 343 | it('should export the module execution result', function () { 344 | var module = {exports: null, id: 'my-module'}; 345 | 346 | loader.load('my/module/path.php', module); 347 | 348 | expect(module.exports).to.equal('my module execution result'); 349 | }); 350 | 351 | describe('when loading bootstraps', function () { 352 | var bootstrapResult, 353 | module, 354 | moduleFactory; 355 | 356 | beforeEach(function () { 357 | bootstrapResult = function () {}; 358 | module = {exports: null, id: 'my-module'}; 359 | moduleFactory = sinon.stub(); 360 | initialiserContext.isLoadingBootstraps.returns(true); 361 | moduleRepository.loadBootstrap.returns(bootstrapResult); 362 | }); 363 | 364 | it('should load the bootstrap from the ModuleRepository', function () { 365 | loader.load('my/module/path.php', module, moduleFactory); 366 | 367 | expect(moduleRepository.loadBootstrap).to.have.been.calledOnce; 368 | expect(moduleRepository.loadBootstrap).to.have.been.calledWith( 369 | 'my/module/path.php', 370 | 'my-module', 371 | moduleFactory 372 | ); 373 | }); 374 | 375 | it('should export the bootstrap result', function () { 376 | loader.load('my/module/path.php', module, moduleFactory); 377 | 378 | expect(module.exports).to.equal(bootstrapResult); 379 | }); 380 | }); 381 | 382 | describe('when in loading-module-factory mode', function () { 383 | var configuredFactory, 384 | module, 385 | moduleFactory; 386 | 387 | beforeEach(function () { 388 | configuredFactory = function () {}; 389 | module = {exports: null, id: 'my-module'}; 390 | moduleFactory = sinon.stub(); 391 | moduleRepository.isLoadingModuleFactoryOnly.returns(true); 392 | moduleRepository.load.returns(configuredFactory); 393 | }); 394 | 395 | it('should load the module via the ModuleRepository', function () { 396 | loader.load('my/module/path.php', module, moduleFactory); 397 | 398 | expect(moduleRepository.load).to.have.been.calledOnce; 399 | expect(moduleRepository.load).to.have.been.calledWith( 400 | 'my/module/path.php', 401 | 'my-module', 402 | moduleFactory 403 | ); 404 | }); 405 | 406 | it('should export the configured factory', function () { 407 | loader.load('my/module/path.php', module, moduleFactory); 408 | 409 | expect(module.exports).to.equal(configuredFactory); 410 | }); 411 | }); 412 | }); 413 | }); 414 | -------------------------------------------------------------------------------- /test/unit/ModuleRepositoryTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var expect = require('chai').expect, 13 | hasOwn = {}.hasOwnProperty, 14 | sinon = require('sinon'), 15 | ModuleRepository = require('../../src/ModuleRepository'); 16 | 17 | describe('ModuleRepository', function () { 18 | var configuredFirstModuleFactory, 19 | configuredSecondModuleFactory, 20 | environment, 21 | moduleFactoryFetcher, 22 | originalFirstModuleFactory, 23 | originalSecondModuleFactory, 24 | repository, 25 | requireCache; 26 | 27 | beforeEach(function () { 28 | environment = {}; 29 | moduleFactoryFetcher = sinon.stub(); 30 | requireCache = { 31 | 'first-module-id': {}, 32 | 'second-module-id': {} 33 | }; 34 | 35 | // First fake module file that does exist 36 | originalFirstModuleFactory = sinon.stub(); 37 | originalFirstModuleFactory.using = sinon.stub(); 38 | configuredFirstModuleFactory = sinon.stub(); 39 | originalFirstModuleFactory.using 40 | .withArgs({path: 'my/first/module/path.php'}) 41 | .returns(configuredFirstModuleFactory); 42 | moduleFactoryFetcher 43 | .withArgs('my/first/module/path.php', false) 44 | .callsFake(function (path) { 45 | return repository.load(path, 'first-module-id', originalFirstModuleFactory); 46 | }); 47 | moduleFactoryFetcher 48 | .withArgs('my/first/module/path.php', true) 49 | .returns(true); 50 | 51 | // Second fake module file that does exist 52 | originalSecondModuleFactory = sinon.stub(); 53 | originalSecondModuleFactory.using = sinon.stub(); 54 | configuredSecondModuleFactory = sinon.stub(); 55 | originalSecondModuleFactory.using 56 | .withArgs({path: 'my/second/module/path.php'}) 57 | .returns(configuredSecondModuleFactory); 58 | moduleFactoryFetcher 59 | .withArgs('my/second/module/path.php', false) 60 | .callsFake(function (path) { 61 | return repository.load(path, 'second-module-id', originalSecondModuleFactory); 62 | }); 63 | moduleFactoryFetcher 64 | .withArgs('my/second/module/path.php', true) 65 | .returns(true); 66 | 67 | // A fake file that does not exist 68 | moduleFactoryFetcher 69 | .withArgs('my/non-existent/module/path.php', true) 70 | .returns(false); 71 | 72 | repository = new ModuleRepository(requireCache); 73 | repository.init(moduleFactoryFetcher); 74 | }); 75 | 76 | describe('getModuleFactory()', function () { 77 | describe('on the initial fetch of a module', function () { 78 | it('should invoke the fetcher correctly', function () { 79 | repository.getModuleFactory('my/first/module/path.php'); 80 | 81 | expect(moduleFactoryFetcher).to.have.been.calledOnce; 82 | expect(moduleFactoryFetcher).to.have.been.calledWith( 83 | 'my/first/module/path.php', 84 | false // Flag indicating that this is not just an existence check 85 | ); 86 | }); 87 | 88 | it('should throw when the module does not exist according to the fetcher', function () { 89 | moduleFactoryFetcher 90 | .withArgs('my/first/module/path.php', false) 91 | .returns(null); 92 | 93 | expect(function () { 94 | repository.getModuleFactory('my/first/module/path.php'); 95 | }).to.throw('File "my/first/module/path.php" is not in the compiled PHP file map'); 96 | }); 97 | 98 | it('should throw when the fetcher does not result in a call to .load()', function () { 99 | moduleFactoryFetcher 100 | .withArgs('my/first/module/path.php', false) 101 | // Do nothing, but also do not return null as that would indicate 102 | // that the module does not exist 103 | .returns({}); 104 | 105 | expect(function () { 106 | repository.getModuleFactory('my/first/module/path.php'); 107 | }).to.throw('Unexpected state: module "my/first/module/path.php" should have been loaded by now'); 108 | }); 109 | 110 | it('should throw when the fetcher does not return the cached configured module factory', function () { 111 | moduleFactoryFetcher 112 | .withArgs('my/first/module/path.php', false) 113 | .callsFake(function (path) { 114 | repository.load(path, 'first-module-id', originalFirstModuleFactory); 115 | 116 | return {}; // Incorrect: this should be returning the configured module factory 117 | }); 118 | 119 | expect(function () { 120 | repository.getModuleFactory('my/first/module/path.php'); 121 | }).to.throw('Unexpected state: factory for module "my/first/module/path.php" loaded incorrectly'); 122 | }); 123 | 124 | it('should throw when the fetcher does not cache the module in require.cache[...] correctly', function () { 125 | delete requireCache['first-module-id']; 126 | 127 | expect(function () { 128 | repository.getModuleFactory('my/first/module/path.php'); 129 | }).to.throw( 130 | 'Path "./my/first/module/path.php" (id "first-module-id") is not in require.cache' 131 | ); 132 | }); 133 | 134 | it('should remove the module from require.cache', function () { 135 | repository.getModuleFactory('my/first/module/path.php'); 136 | 137 | expect(requireCache).not.to.have.property('first-module-id'); 138 | }); 139 | 140 | it('should return the configured module factory', function () { 141 | expect(repository.getModuleFactory('my/first/module/path.php')).to.equal(configuredFirstModuleFactory); 142 | }); 143 | }); 144 | 145 | describe('on subsequent fetches of a module', function () { 146 | beforeEach(function () { 147 | // Perform the initial fetch. 148 | repository.getModuleFactory('my/first/module/path.php'); 149 | }); 150 | 151 | it('should return the cached configured module factory', function () { 152 | expect(repository.getModuleFactory('my/first/module/path.php')) 153 | .to.equal(configuredFirstModuleFactory); 154 | }); 155 | 156 | it('should not invoke the fetcher again', function () { 157 | moduleFactoryFetcher.resetHistory(); 158 | 159 | repository.getModuleFactory('my/first/module/path.php'); 160 | 161 | expect(moduleFactoryFetcher).not.to.have.been.called; 162 | }); 163 | }); 164 | }); 165 | 166 | describe('init()', function () { 167 | it('should set the module factory fetcher', function () { 168 | var newFetcher = sinon.stub(); 169 | repository.init(newFetcher); 170 | 171 | moduleFactoryFetcher.resetHistory(); 172 | repository.moduleExists('my/first/module/path.php'); 173 | 174 | expect(moduleFactoryFetcher).not.to.have.been.called; 175 | expect(newFetcher).to.have.been.calledOnce; 176 | expect(newFetcher).to.have.been.calledWith('my/first/module/path.php', true); 177 | }); 178 | }); 179 | 180 | describe('isLoadingModuleFactoryOnly()', function () { 181 | it('should return the current loading module factory only state', function () { 182 | // Default state should be false. 183 | expect(repository.isLoadingModuleFactoryOnly()).to.be.false; 184 | repository.getModuleFactory('my/first/module/path.php'); 185 | // State should be back to false after operation completes. 186 | expect(repository.isLoadingModuleFactoryOnly()).to.be.false; 187 | }); 188 | }); 189 | 190 | describe('load()', function () { 191 | var configuredModuleFactory, 192 | originalModuleFactory; 193 | 194 | beforeEach(function () { 195 | originalModuleFactory = sinon.stub(); 196 | configuredModuleFactory = sinon.stub(); 197 | 198 | originalModuleFactory.using = sinon.stub().returns(configuredModuleFactory); 199 | }); 200 | 201 | it('should return a configured factory', function () { 202 | var loadResult; 203 | moduleFactoryFetcher 204 | .withArgs('my/first/module/path.php', false) 205 | .callsFake(function (path) { 206 | loadResult = repository.load(path, 'first-module-id', originalModuleFactory); 207 | return loadResult; 208 | }); 209 | 210 | repository.getModuleFactory('my/first/module/path.php'); 211 | 212 | expect(moduleFactoryFetcher).to.have.been.calledOnce; 213 | expect(loadResult).to.equal(configuredModuleFactory); 214 | }); 215 | 216 | it('should correctly configure the factory with the module path and id', function () { 217 | repository.load('my/first/module/path.php', 'first-module-id', originalModuleFactory); 218 | 219 | expect(originalModuleFactory.using).to.have.been.calledOnce; 220 | expect(originalModuleFactory.using).to.have.been.calledWith({ 221 | path: 'my/first/module/path.php' 222 | }); 223 | }); 224 | 225 | it('should pass the path of the module to [moduleFactory].using(...) as an option', function () { 226 | repository.load('my/third/module/path.php', 'second-module-id', originalModuleFactory); 227 | 228 | expect(originalModuleFactory.using).to.have.been.calledWith({ 229 | path: 'my/third/module/path.php' 230 | }); 231 | }); 232 | }); 233 | 234 | describe('loadBootstrap()', function () { 235 | var bootstrapModuleFactory, 236 | configuredFactory, 237 | engine, 238 | bootstrapResult; 239 | 240 | beforeEach(function () { 241 | engine = { 242 | execute: sinon.stub() 243 | }; 244 | bootstrapResult = {}; 245 | engine.execute.returns(bootstrapResult); 246 | 247 | bootstrapModuleFactory = sinon.stub(); 248 | configuredFactory = sinon.stub(); 249 | configuredFactory.returns(engine); 250 | 251 | bootstrapModuleFactory.using = sinon.stub().returns(configuredFactory); 252 | }); 253 | 254 | it('should configure the factory with the module path', function () { 255 | repository.loadBootstrap('my/bootstrap/path.php', 'bootstrap-id', bootstrapModuleFactory); 256 | 257 | expect(bootstrapModuleFactory.using).to.have.been.calledOnce; 258 | expect(bootstrapModuleFactory.using).to.have.been.calledWith({ 259 | path: 'my/bootstrap/path.php' 260 | }); 261 | }); 262 | 263 | it('should store the bootstrap in configuredModules', function () { 264 | var bootstrapFn = repository.loadBootstrap('my/bootstrap/path.php', 'bootstrap-id', bootstrapModuleFactory), 265 | result; 266 | 267 | expect(repository.moduleExists('my/bootstrap/path.php')).to.be.true; 268 | 269 | // Add the bootstrap to the require cache to simulate what would happen in real usage. 270 | requireCache['bootstrap-id'] = { 271 | exports: configuredFactory 272 | }; 273 | 274 | result = bootstrapFn(environment); 275 | 276 | expect(hasOwn.call(requireCache, 'bootstrap-id')).to.be.false; 277 | expect(configuredFactory).to.have.been.calledWith({}, environment); 278 | expect(engine.execute).to.have.been.calledOnce; 279 | expect(result).to.equal(bootstrapResult); 280 | }); 281 | }); 282 | 283 | describe('moduleExists()', function () { 284 | describe('when the module\'s factory has not been fetched', function () { 285 | it('should return true when the module factory fetcher indicates', function () { 286 | expect(repository.moduleExists('my/first/module/path.php')).to.be.true; 287 | }); 288 | 289 | it('should return false when the module factory fetcher indicates', function () { 290 | expect(repository.moduleExists('my/non-existent/module/path.php')).to.be.false; 291 | }); 292 | }); 293 | 294 | describe('when the module\'s factory has already been fetched', function () { 295 | it('should return true', function () { 296 | repository.getModuleFactory('my/first/module/path.php'); 297 | 298 | expect(repository.moduleExists('my/first/module/path.php')).to.be.true; 299 | }); 300 | }); 301 | }); 302 | 303 | describe('unrequire()', function () { 304 | it('should throw when the module is not loaded', function () { 305 | expect(function () { 306 | repository.unrequire('non/existent/module.php'); 307 | }).to.throw('Module "non/existent/module.php" is not loaded'); 308 | }); 309 | 310 | it('should throw when the module is not in require.cache', function () { 311 | repository.load('my/third/module/path.php', 'third-module-id', originalFirstModuleFactory); 312 | delete requireCache['third-module-id']; 313 | 314 | expect(function () { 315 | repository.unrequire('my/third/module/path.php'); 316 | }).to.throw('Path "./my/third/module/path.php" (id "third-module-id") is not in require.cache'); 317 | }); 318 | 319 | it('should remove the module from require.cache', function () { 320 | repository.load('my/third/module/path.php', 'third-module-id', originalFirstModuleFactory); 321 | requireCache['third-module-id'] = {}; 322 | 323 | repository.unrequire('my/third/module/path.php'); 324 | 325 | expect(hasOwn.call(requireCache, 'third-module-id')).to.be.false; 326 | }); 327 | }); 328 | }); 329 | -------------------------------------------------------------------------------- /test/unit/PerformanceTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var expect = require('chai').expect, 13 | sinon = require('sinon'), 14 | Performance = require('../../src/Performance'); 15 | 16 | describe('Performance', function () { 17 | var Date, 18 | global, 19 | nativePerformance, 20 | performance; 21 | 22 | beforeEach(function () { 23 | Date = sinon.stub(); 24 | nativePerformance = { 25 | now: sinon.stub(), 26 | timing: { 27 | navigationStart: 21 28 | } 29 | }; 30 | global = { 31 | performance: nativePerformance 32 | }; 33 | Date.prototype.getTime = sinon.stub(); 34 | 35 | performance = new Performance(Date, global); 36 | }); 37 | 38 | describe('getTimeInMicroseconds()', function () { 39 | it('should return the result from Window.performance.now() + navigationStart where supported', function () { 40 | // Current time in milliseconds, accurate to the nearest microsecond 41 | nativePerformance.now.returns(1000000); 42 | 43 | expect(performance.getTimeInMicroseconds()).to.equal(1000021000); 44 | }); 45 | 46 | it('should return the current time in us rounded to the nearest ms when not supported', function () { 47 | delete global.performance; 48 | // Current time in milliseconds, accurate to the nearest millisecond 49 | Date.prototype.getTime.returns(12345); 50 | 51 | expect(performance.getTimeInMicroseconds()).to.equal(12345000); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/unit/TransformerTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PHPify - Compiles PHP modules to CommonJS with Uniter 3 | * Copyright (c) Dan Phillimore (asmblah) 4 | * https://github.com/uniter/phpify 5 | * 6 | * Released under the MIT license 7 | * https://github.com/uniter/phpify/raw/master/MIT-LICENSE.txt 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var expect = require('chai').expect, 13 | nowdoc = require('nowdoc'), 14 | sinon = require('sinon'), 15 | Transformer = require('../../src/Transformer'); 16 | 17 | describe('Transformer', function () { 18 | var globby, 19 | initialiserStubPath, 20 | parserState, 21 | phpCoreConfig, 22 | phpifyConfig, 23 | phpParser, 24 | phpToJS, 25 | phpToJSConfig, 26 | phpToJSResult, 27 | resolveRequire, 28 | transformer, 29 | transpilerConfig; 30 | 31 | beforeEach(function () { 32 | globby = { 33 | sync: sinon.stub().returns([]) 34 | }; 35 | initialiserStubPath = '/my/path/to/my/initialiser_stub.php'; 36 | parserState = { 37 | setPath: sinon.stub() 38 | }; 39 | phpCoreConfig = { 40 | topLevelConfig: { 41 | my: 'top level config' 42 | }, 43 | pluginConfigFilePaths: ['/my/first_plugin', '/my/second_plugin'] 44 | }; 45 | phpifyConfig = { 46 | include: ['**/*.php'] 47 | }; 48 | phpParser = { 49 | getState: sinon.stub().returns(parserState), 50 | parse: sinon.stub() 51 | }; 52 | phpToJS = { 53 | transpile: sinon.stub() 54 | }; 55 | phpToJSConfig = {}; 56 | phpToJSResult = { 57 | code: '(function () { return "my transpiled JS code"; }())', 58 | map: {} 59 | }; 60 | transpilerConfig = { 61 | transpilerRule1: 'value 1', 62 | transpilerRule2: 'value 2' 63 | }; 64 | resolveRequire = sinon.stub(); 65 | 66 | phpToJS.transpile.returns(phpToJSResult); 67 | resolveRequire.withArgs('phpify').returns('/my/path/to/node_modules/phpify/api'); 68 | resolveRequire.withArgs('phpruntime').returns('/my/path/to/node_modules/phpruntime/index.js'); 69 | 70 | transformer = new Transformer( 71 | phpParser, 72 | phpToJS, 73 | resolveRequire, 74 | globby, 75 | initialiserStubPath, 76 | phpifyConfig, 77 | phpToJSConfig, 78 | transpilerConfig, 79 | phpCoreConfig, 80 | '/my/context/dir' 81 | ); 82 | }); 83 | 84 | describe('for the initialiser stub file', function () { 85 | beforeEach(function () { 86 | phpifyConfig.include = [ 87 | 'my/first/**/*.php', 88 | 'my/second/**/*.php', 89 | '!my/third/**/*.php' 90 | ]; 91 | 92 | globby.sync.withArgs([ 93 | '/my/context/dir/my/first/**/*.php', 94 | '/my/context/dir/my/second/**/*.php', 95 | '!/my/context/dir/my/third/**/*.php' 96 | ]).returns([ 97 | '/my/first/matched/file.php', 98 | '/my/second/matched/file.php' 99 | ]); 100 | }); 101 | 102 | it('should return the initialiser code, including the virtual FS switch, when no bootstraps are defined', function () { 103 | var result = transformer.transform('' 187 | }; 188 | 189 | var result = transformer.transform('"}); 221 | }; 222 | EOS*/;}), // jshint ignore:line 223 | map: null 224 | }); 225 | }); 226 | }); 227 | 228 | describe('for normal files that aren\'t the initialiser stub', function () { 229 | it('should return the result from the transpiler', function () { 230 | phpParser.parse 231 | .withArgs('